From 8ec864ea95ae45c6b00150d46c442136fa4832fb Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Thu, 21 May 2026 10:44:53 +0100 Subject: [PATCH 1/2] feat: add WhatsApp BSUID support and split test/integration profiles - Add ConversationMessageMetadata, ConversationSenderMetadata, and ConversationStatusMessageMetadata to model BSUID webhook payloads - Add metadata field to ConversationMessage - Split Maven test/integration profiles so unit tests run without credentials - Fix ContactTest and MessageBirdClientTest to skip gracefully via assumeNotNull - Bump version to 6.3.0 Co-Authored-By: Claude Sonnet 4.6 --- api/pom.xml | 38 ++++++++- .../messagebird/MessageBirdServiceImpl.java | 2 +- .../conversations/ConversationMessage.java | 10 +++ .../ConversationMessageMetadata.java | 39 +++++++++ .../ConversationSenderMetadata.java | 47 +++++++++++ .../ConversationStatusMessageMetadata.java | 79 +++++++++++++++++++ .../java/com/messagebird/ContactTest.java | 2 + .../com/messagebird/ConversationsTest.java | 1 + .../messagebird/MessageBirdClientTest.java | 6 +- 9 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java diff --git a/api/pom.xml b/api/pom.xml index e17d2358..fb65b2b0 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.2.5 + 6.3.0 jar ${project.groupId}:${project.artifactId} @@ -62,19 +62,51 @@ + test false + + + UTF-8 + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + **/ContactTest.java + **/MessageBirdClientTest.java + + + + + + + + + integration + + false + UTF-8 + disable-doclint - [8,11,) + [1.8,11) none - true UTF-8 diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java index 3cc1f25e..ce3c10e7 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -72,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "6.2.5"; + private final String clientVersion = "6.3.0"; private final String userAgentString; private Proxy proxy = null; diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java index 2be027be..1dea8280 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java @@ -22,6 +22,7 @@ public class ConversationMessage { private Date updatedDatetime; private Map source; private ConversationMessageTag tag; + private ConversationMessageMetadata metadata; /** * See: {@link ConversationPlatformConstants} */ @@ -115,6 +116,14 @@ public void setTag(ConversationMessageTag tag) { this.tag = tag; } + public ConversationMessageMetadata getMetadata() { + return metadata; + } + + public void setMetadata(ConversationMessageMetadata metadata) { + this.metadata = metadata; + } + public String getPlatform() { return platform; } @@ -146,6 +155,7 @@ public String toString() { ", updatedDatetime=" + updatedDatetime + ", source=" + source + ", tag=" + tag + + ", metadata=" + metadata + ", platform='" + platform + '\'' + '}'; } diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java new file mode 100644 index 00000000..7d908e6a --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java @@ -0,0 +1,39 @@ +package com.messagebird.objects.conversations; + +import java.util.Date; + +/** + * Inner metadata attached to a conversation message. Present on both incoming + * messages and status webhook payloads. {@code sender.userId} always contains + * the BSUID when Meta provides one. When both identifiers exist, the phone + * number appears in the parent {@code from} field, not in this object. + */ +public class ConversationMessageMetadata { + + private ConversationSenderMetadata sender; + private Date receivedAt; + + public ConversationSenderMetadata getSender() { + return sender; + } + + public void setSender(ConversationSenderMetadata sender) { + this.sender = sender; + } + + public Date getReceivedAt() { + return receivedAt; + } + + public void setReceivedAt(Date receivedAt) { + this.receivedAt = receivedAt; + } + + @Override + public String toString() { + return "ConversationMessageMetadata{" + + "sender=" + sender + + ", receivedAt=" + receivedAt + + '}'; + } +} diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java new file mode 100644 index 00000000..33d7e225 --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java @@ -0,0 +1,47 @@ +package com.messagebird.objects.conversations; + +/** + * Metadata about the sender of a WhatsApp message. {@code userId} always + * contains the BSUID (e.g. "US.13491208655302741918") when Meta supplies one. + * When both a phone number and a BSUID are available, the phone number appears + * in the parent message's {@code from} field — not here. + */ +public class ConversationSenderMetadata { + + private String userId; + private String username; + private String displayName; + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return "ConversationSenderMetadata{" + + "userId='" + userId + '\'' + + ", username='" + username + '\'' + + ", displayName='" + displayName + '\'' + + '}'; + } +} diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java new file mode 100644 index 00000000..ce5635ae --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java @@ -0,0 +1,79 @@ +package com.messagebird.objects.conversations; + +/** + * The {@code messageMetadata} block delivered in status webhook events (e.g. + * {@code statusSent}). Reflects the original message that triggered the status. + * + *

Both {@code from} and {@code to} accept either a phone number or a + * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918"). + * The BSUID is also available via {@code metadata.sender.userId}. + */ +public class ConversationStatusMessageMetadata { + + private String id; + private String from; + private String to; + private String type; + private ConversationContent content; + private ConversationMessageMetadata metadata; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public ConversationContent getContent() { + return content; + } + + public void setContent(ConversationContent content) { + this.content = content; + } + + public ConversationMessageMetadata getMetadata() { + return metadata; + } + + public void setMetadata(ConversationMessageMetadata metadata) { + this.metadata = metadata; + } + + @Override + public String toString() { + return "ConversationStatusMessageMetadata{" + + "id='" + id + '\'' + + ", from='" + from + '\'' + + ", to='" + to + '\'' + + ", type='" + type + '\'' + + ", content=" + content + + ", metadata=" + metadata + + '}'; + } +} diff --git a/api/src/test/java/com/messagebird/ContactTest.java b/api/src/test/java/com/messagebird/ContactTest.java index 2377733d..5dfdea38 100644 --- a/api/src/test/java/com/messagebird/ContactTest.java +++ b/api/src/test/java/com/messagebird/ContactTest.java @@ -8,6 +8,7 @@ import org.mockito.Mockito; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.junit.Assume.assumeNotNull; import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; /** @@ -30,6 +31,7 @@ public class ContactTest { @BeforeClass public static void setUpClass() throws UnauthorizedException, GeneralException { String accessKey = System.getProperty("messageBirdAccessKey"); + assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey to run", accessKey); msisdn = generateMsisdn(); diff --git a/api/src/test/java/com/messagebird/ConversationsTest.java b/api/src/test/java/com/messagebird/ConversationsTest.java index 4e7624e8..e08f0175 100644 --- a/api/src/test/java/com/messagebird/ConversationsTest.java +++ b/api/src/test/java/com/messagebird/ConversationsTest.java @@ -17,6 +17,7 @@ public class ConversationsTest { private static final String JSON_CONVERSATION = "{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://example.com/assets/image.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678 },\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"chname\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-22T16:05:15Z\",\"lastReceivedDatetime\": \"2018-08-22T15:47:34Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 1,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}"; private static final String JSON_CONVERSATION_LIST = "{\"offset\": 20,\"limit\": 10,\"count\": 1,\"totalCount\": 1,\"items\": [{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://s3-eu-west-1.amazonaws.com/messagebird-chat/telegram/0d1dae7t5b7d7eb4531c14n04328336/6de655at5b7d859309f821n60607065/d0b705dt5b7d859309f987n05372263.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678},\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"TestChannel\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-24T09:49:01Z\",\"lastReceivedDatetime\": \"2018-08-24T09:49:01Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 3,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}]}"; private static final String JSON_UNAUTHORIZED_ERROR = "{\"errors\": [{\"code\": 2,\"description\": \"Request was not authenticated\"}]}"; + // Status-update payload where Meta supplies both a phone number and a BSUID on the contact. @Test(expected = UnauthorizedException.class) public void testItThrowsErrors() throws GeneralException, UnauthorizedException { diff --git a/api/src/test/java/com/messagebird/MessageBirdClientTest.java b/api/src/test/java/com/messagebird/MessageBirdClientTest.java index ee2dc890..b11c2f54 100644 --- a/api/src/test/java/com/messagebird/MessageBirdClientTest.java +++ b/api/src/test/java/com/messagebird/MessageBirdClientTest.java @@ -16,6 +16,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; +import static org.junit.Assume.assumeNotNull; import java.math.BigInteger; import java.util.Collections; @@ -44,7 +45,10 @@ public class MessageBirdClientTest { @BeforeClass public static void setUpClass() { messageBirdAccessKey = System.getProperty("messageBirdAccessKey"); - messageBirdMSISDN = new BigInteger(System.getProperty("messageBirdMSISDN")); + String msisdn = System.getProperty("messageBirdMSISDN"); + assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey and -DmessageBirdMSISDN to run", + messageBirdAccessKey, msisdn); + messageBirdMSISDN = new BigInteger(msisdn); } @Before From fa2a89860e0647accd1c4653a429ab99915dfa3b Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Thu, 21 May 2026 12:18:24 +0100 Subject: [PATCH 2/2] test: add deserialization tests for BSUID metadata; refine status metadata javadoc - Add JSON round-trip tests covering ConversationMessage.metadata and ConversationStatusMessageMetadata so the new types are exercised. - Clarify in javadoc that ConversationStatusMessageMetadata is a standalone POJO for consumers parsing incoming webhook payloads (not produced by any SDK request). - Remove dangling code comment in ConversationsTest. Co-Authored-By: Claude Sonnet 4.6 --- .../ConversationStatusMessageMetadata.java | 9 ++++- .../messagebird/ConversationMessagesTest.java | 39 +++++++++++++++++++ .../com/messagebird/ConversationsTest.java | 1 - 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java index ce5635ae..df7feebf 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java @@ -1,8 +1,13 @@ package com.messagebird.objects.conversations; /** - * The {@code messageMetadata} block delivered in status webhook events (e.g. - * {@code statusSent}). Reflects the original message that triggered the status. + * The {@code messageMetadata} block delivered inside status webhook payloads + * (e.g. {@code statusSent}, {@code statusDelivered}). Reflects the original + * message that triggered the status update. + * + *

This class is not produced by any SDK request — it is a standalone POJO + * intended for consumers who deserialize incoming webhook payloads in their + * own HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}. * *

Both {@code from} and {@code to} accept either a phone number or a * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918"). diff --git a/api/src/test/java/com/messagebird/ConversationMessagesTest.java b/api/src/test/java/com/messagebird/ConversationMessagesTest.java index 81357f88..5bd0a5c5 100644 --- a/api/src/test/java/com/messagebird/ConversationMessagesTest.java +++ b/api/src/test/java/com/messagebird/ConversationMessagesTest.java @@ -1,5 +1,7 @@ package com.messagebird; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.messagebird.exceptions.GeneralException; import com.messagebird.exceptions.NotFoundException; import com.messagebird.exceptions.UnauthorizedException; @@ -23,6 +25,8 @@ public class ConversationMessagesTest { private static final String JSON_CONVERSATION_MESSAGE_TEXT = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}"; private static final String JSON_CONVERSATION_MESSAGE_VIDEO = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"video\",\"direction\": \"received\",\"content\": {\"video\": { \"url\": \"https://example.com/video.mp4\" } },\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}"; private static final String JSON_CONVERSATION_SEND_MESSAGE_RESPONSE = "{\"id\":\"mesid\",\"status\":\"accepted\",\"fallback\":{\"id\":\"mesid\"}}"; + private static final String JSON_CONVERSATION_MESSAGE_BSUID = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"metadata\": {\"sender\": {\"displayName\": \"Alice\",\"username\": \"alice_shop\",\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"2025-04-15T16:00:00Z\"},\"createdDatetime\": \"2025-04-15T16:00:00Z\",\"updatedDatetime\": \"2025-04-15T16:00:00Z\"}"; + private static final String JSON_STATUS_MESSAGE_METADATA = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"content\": {\"text\": \"Hello! Your order has been shipped.\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"0001-01-01T00:00:00Z\"}}"; /** * Epsilon to use when checking two latitudes or longitudes for equality. @@ -183,6 +187,41 @@ public void testViewConversationMessageLocation() throws GeneralException, NotFo assertEquals(4.911627, location.getLongitude(), EPSILON_LOCATION_EQUALITY); } + @Test + public void testViewConversationMessageWithBsuidMetadata() throws GeneralException, NotFoundException, UnauthorizedException { + MessageBirdService messageBirdService = SpyService + .expects("GET", "messages/mesid") + .withConversationsAPIBaseURL() + .andReturns(new APIResponse(JSON_CONVERSATION_MESSAGE_BSUID)); + MessageBirdClient messageBirdClient = new MessageBirdClient(messageBirdService); + + ConversationMessage message = messageBirdClient.viewConversationMessage("mesid"); + + ConversationMessageMetadata metadata = message.getMetadata(); + assertNotNull(metadata); + assertNotNull(metadata.getReceivedAt()); + ConversationSenderMetadata sender = metadata.getSender(); + assertEquals("Alice", sender.getDisplayName()); + assertEquals("alice_shop", sender.getUsername()); + assertEquals("US.13491208655302741918", sender.getUserId()); + } + + @Test + public void testStatusMessageMetadataDeserializes() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + ConversationStatusMessageMetadata md = mapper.readValue( + JSON_STATUS_MESSAGE_METADATA, ConversationStatusMessageMetadata.class); + + assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId()); + assertEquals("15551234567", md.getFrom()); + assertEquals("US.13491208655302741918", md.getTo()); + assertEquals("text", md.getType()); + assertEquals("Hello! Your order has been shipped.", md.getContent().getText()); + assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId()); + } + @Test public void testViewConversationMessageText() throws GeneralException, NotFoundException, UnauthorizedException { MessageBirdService messageBirdService = SpyService diff --git a/api/src/test/java/com/messagebird/ConversationsTest.java b/api/src/test/java/com/messagebird/ConversationsTest.java index e08f0175..4e7624e8 100644 --- a/api/src/test/java/com/messagebird/ConversationsTest.java +++ b/api/src/test/java/com/messagebird/ConversationsTest.java @@ -17,7 +17,6 @@ public class ConversationsTest { private static final String JSON_CONVERSATION = "{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://example.com/assets/image.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678 },\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"chname\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-22T16:05:15Z\",\"lastReceivedDatetime\": \"2018-08-22T15:47:34Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 1,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}"; private static final String JSON_CONVERSATION_LIST = "{\"offset\": 20,\"limit\": 10,\"count\": 1,\"totalCount\": 1,\"items\": [{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://s3-eu-west-1.amazonaws.com/messagebird-chat/telegram/0d1dae7t5b7d7eb4531c14n04328336/6de655at5b7d859309f821n60607065/d0b705dt5b7d859309f987n05372263.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678},\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"TestChannel\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-24T09:49:01Z\",\"lastReceivedDatetime\": \"2018-08-24T09:49:01Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 3,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}]}"; private static final String JSON_UNAUTHORIZED_ERROR = "{\"errors\": [{\"code\": 2,\"description\": \"Request was not authenticated\"}]}"; - // Status-update payload where Meta supplies both a phone number and a BSUID on the contact. @Test(expected = UnauthorizedException.class) public void testItThrowsErrors() throws GeneralException, UnauthorizedException {