From 7ae76ef8ee1dfaaa1c5be22658ca1146816a12db Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Mon, 12 Feb 2024 14:50:08 +0000 Subject: [PATCH] feat: Add from to WhatsappCodelessWorkflow --- .../verify2/AbstractWhatsappWorkflow.java | 61 ++++++++++++ .../client/verify2/SilentAuthWorkflow.java | 2 +- .../vonage/client/verify2/SmsWorkflow.java | 2 +- .../client/verify2/VerificationRequest.java | 2 +- .../verify2/WhatsappCodelessWorkflow.java | 41 ++++++++- .../client/verify2/WhatsappWorkflow.java | 34 +++---- .../verify2/VerificationRequestTest.java | 92 ++++++++++--------- .../client/verify2/Verify2ClientTest.java | 19 ++-- 8 files changed, 173 insertions(+), 80 deletions(-) create mode 100644 src/main/java/com/vonage/client/verify2/AbstractWhatsappWorkflow.java diff --git a/src/main/java/com/vonage/client/verify2/AbstractWhatsappWorkflow.java b/src/main/java/com/vonage/client/verify2/AbstractWhatsappWorkflow.java new file mode 100644 index 000000000..85eff8b9b --- /dev/null +++ b/src/main/java/com/vonage/client/verify2/AbstractWhatsappWorkflow.java @@ -0,0 +1,61 @@ +/* + * 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.verify2; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.vonage.client.common.E164; + +/** + * Intermediate class for WhatsApp workflows. + * + * @since 8.3.0 + */ +@JsonInclude(value = JsonInclude.Include.NON_NULL) +abstract class AbstractWhatsappWorkflow extends AbstractNumberWorkflow { + + protected AbstractWhatsappWorkflow(Builder builder) { + super(builder); + } + + @Override + protected String validateFrom(String from) { + // TODO: remove this when removing deprecated constructors + if (from == null) return null; + return new E164(super.validateFrom(from)).toString(); + } + + /** + * The number to send the verification request from. + * + * @return The sender WABA number in E.164 format. + */ + @JsonProperty("from") + public String getFrom() { + return from; + } + + protected abstract static class Builder< + N extends AbstractWhatsappWorkflow, + B extends AbstractWhatsappWorkflow.Builder + > extends AbstractNumberWorkflow.Builder { + + protected Builder(Channel channel, String to, String from) { + super(channel, to); + from(from); + } + } +} diff --git a/src/main/java/com/vonage/client/verify2/SilentAuthWorkflow.java b/src/main/java/com/vonage/client/verify2/SilentAuthWorkflow.java index c59e6314c..8c42246ec 100644 --- a/src/main/java/com/vonage/client/verify2/SilentAuthWorkflow.java +++ b/src/main/java/com/vonage/client/verify2/SilentAuthWorkflow.java @@ -115,7 +115,7 @@ public static final class Builder extends AbstractNumberWorkflow.Builder { private String from, appHash, contentId, entityId; - Builder(String to) { + private Builder(String to) { super(Channel.SMS, to); } diff --git a/src/main/java/com/vonage/client/verify2/VerificationRequest.java b/src/main/java/com/vonage/client/verify2/VerificationRequest.java index fe09aa114..4f7aa9ecc 100644 --- a/src/main/java/com/vonage/client/verify2/VerificationRequest.java +++ b/src/main/java/com/vonage/client/verify2/VerificationRequest.java @@ -200,7 +200,7 @@ public static final class Builder { Locale locale; List workflows = new ArrayList<>(1); - Builder() {} + private Builder() {} /** * (REQUIRED) diff --git a/src/main/java/com/vonage/client/verify2/WhatsappCodelessWorkflow.java b/src/main/java/com/vonage/client/verify2/WhatsappCodelessWorkflow.java index 7922cd091..77aa19039 100644 --- a/src/main/java/com/vonage/client/verify2/WhatsappCodelessWorkflow.java +++ b/src/main/java/com/vonage/client/verify2/WhatsappCodelessWorkflow.java @@ -23,18 +23,51 @@ * * WhatsApp Interactive guide for an overview of how this works. *

- * By default, WhatsApp messages will be sent using a Vonage WhatsApp Business Account (WABA). - * Please contact sales in order to configure Verify v2 to use your company’s WABA. + * You must have a WhatsApp Business Account configured to use the {@code from} field, which + * is now a requirement for WhatsApp workflows. */ @JsonInclude(value = JsonInclude.Include.NON_NULL) -public final class WhatsappCodelessWorkflow extends AbstractNumberWorkflow { +public final class WhatsappCodelessWorkflow extends AbstractWhatsappWorkflow { + + WhatsappCodelessWorkflow(Builder builder) { + super(builder); + } /** * Constructs a new WhatsApp interactive verification workflow. * * @param to The number to send the verification prompt to, in E.164 format. + * @deprecated This no longer works and will be removed in a future release. */ + @Deprecated public WhatsappCodelessWorkflow(String to) { - super(Channel.WHATSAPP_INTERACTIVE, to); + this(to, null); + } + + /** + * Constructs a new WhatsApp interactive verification workflow. + * + * @param to The number to send the verification prompt to, in E.164 format. + * @param from The WhatsApp Business Account number to send the message from, in E.164 format. + * @since 8.3.0 + */ + public WhatsappCodelessWorkflow(String to, String from) { + this(builder(to, from)); + } + + static Builder builder(String to, String from) { + return new Builder(to, from); + } + + static class Builder extends AbstractWhatsappWorkflow.Builder { + + private Builder(String to, String from) { + super(Channel.WHATSAPP_INTERACTIVE, to, from); + } + + @Override + public WhatsappCodelessWorkflow build() { + return new WhatsappCodelessWorkflow(this); + } } } diff --git a/src/main/java/com/vonage/client/verify2/WhatsappWorkflow.java b/src/main/java/com/vonage/client/verify2/WhatsappWorkflow.java index 404acb0f4..0b8369739 100644 --- a/src/main/java/com/vonage/client/verify2/WhatsappWorkflow.java +++ b/src/main/java/com/vonage/client/verify2/WhatsappWorkflow.java @@ -16,16 +16,15 @@ package com.vonage.client.verify2; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; /** * Defines properties for sending a verification code to a user over a WhatsApp message. *

- * By default, WhatsApp messages will be sent using a Vonage WhatsApp Business Account (WABA). - * Please contact sales in order to configure Verify v2 to use your company’s WABA. + * You must have a WhatsApp Business Account configured to use the {@code from} field, which + * is now a requirement for WhatsApp workflows. */ @JsonInclude(value = JsonInclude.Include.NON_NULL) -public final class WhatsappWorkflow extends AbstractNumberWorkflow { +public final class WhatsappWorkflow extends AbstractWhatsappWorkflow { WhatsappWorkflow(Builder builder) { super(builder); @@ -35,7 +34,9 @@ public final class WhatsappWorkflow extends AbstractNumberWorkflow { * Constructs a new WhatsApp verification workflow. * * @param to The number to send the message to, in E.164 format. + * @deprecated This no longer works and will be removed in a future release. */ + @Deprecated public WhatsappWorkflow(String to) { this(to, null); } @@ -44,31 +45,20 @@ public WhatsappWorkflow(String to) { * Constructs a new WhatsApp verification workflow with a custom sender number. * * @param to The number to send the message to, in E.164 format. - * @param from The number to send the message from, in E.164 format. - * Note that you will need to get in touch with the Vonage sales team to enable use of the field. + * @param from The WhatsApp Business Account number to send the message from, in E.164 format. */ public WhatsappWorkflow(String to, String from) { - this(builder(to).from(from)); + this(builder(to, from)); } - /** - * The number to send the verification request from, if configured. - * - * @return The sender phone number, or {@code null} if unset. - */ - @JsonProperty("from") - public String getFrom() { - return from; - } - - static Builder builder(String to) { - return new Builder(to); + static Builder builder(String to, String from) { + return new Builder(to, from); } - static class Builder extends AbstractNumberWorkflow.Builder { + static class Builder extends AbstractWhatsappWorkflow.Builder { - Builder(String to) { - super(Channel.WHATSAPP, to); + Builder(String to, String from) { + super(Channel.WHATSAPP, to, from); } @Override diff --git a/src/test/java/com/vonage/client/verify2/VerificationRequestTest.java b/src/test/java/com/vonage/client/verify2/VerificationRequestTest.java index e0cd60ff1..65a1cd906 100644 --- a/src/test/java/com/vonage/client/verify2/VerificationRequestTest.java +++ b/src/test/java/com/vonage/client/verify2/VerificationRequestTest.java @@ -18,8 +18,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.VonageUnexpectedException; import com.vonage.client.verify2.VerificationRequest.Builder; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; import java.util.Arrays; import java.util.Collections; import java.util.Locale; @@ -31,7 +31,7 @@ public class VerificationRequestTest { static final String BRAND = "Vonage", TO_NUMBER = "447700900000", - FROM_NUMBER = "447900100002", + FROM_NUMBER = "14157386102", TO_EMAIL = "alice@example.org", FROM_EMAIL = "bob@example.org", CLIENT_REF = "my-personal-reference", @@ -50,38 +50,25 @@ Builder newBuilderAllParams() { } Workflow getWorkflowRequiredParamsForChannel(Channel channel) { - switch (channel) { - default: throw new IllegalStateException(); - case SMS: - return new SmsWorkflow(TO_NUMBER); - case VOICE: - return new VoiceWorkflow(TO_NUMBER); - case WHATSAPP: - return new WhatsappWorkflow(TO_NUMBER); - case WHATSAPP_INTERACTIVE: - return new WhatsappCodelessWorkflow(TO_NUMBER); - case EMAIL: - return new EmailWorkflow(TO_EMAIL); - case SILENT_AUTH: - return new SilentAuthWorkflow(TO_NUMBER); - } + return switch (channel) { + case SMS -> new SmsWorkflow(TO_NUMBER); + case VOICE -> new VoiceWorkflow(TO_NUMBER); + case WHATSAPP -> WhatsappWorkflow.builder(TO_NUMBER, FROM_NUMBER).build(); + case WHATSAPP_INTERACTIVE -> WhatsappCodelessWorkflow.builder(TO_NUMBER, FROM_NUMBER).build(); + case EMAIL -> new EmailWorkflow(TO_EMAIL); + case SILENT_AUTH -> new SilentAuthWorkflow(TO_NUMBER); + }; } Workflow getWorkflowAllParamsForChannel(Channel channel) { - switch (channel) { - case SILENT_AUTH: - return new SilentAuthWorkflow(TO_NUMBER, SANDBOX, REDIRECT_URL); - case SMS: - return SmsWorkflow.builder(TO_NUMBER) - .contentId(CONTENT_ID).entityId(ENTITY_ID) - .from(FROM_NUMBER).appHash(APP_HASH).build(); - case WHATSAPP: - return WhatsappWorkflow.builder(TO_NUMBER).from(FROM_NUMBER).build(); - case EMAIL: - return new EmailWorkflow(TO_EMAIL, FROM_EMAIL); - default: - return getWorkflowRequiredParamsForChannel(channel); - } + return switch (channel) { + case SILENT_AUTH -> new SilentAuthWorkflow(TO_NUMBER, SANDBOX, REDIRECT_URL); + case SMS -> SmsWorkflow.builder(TO_NUMBER) + .contentId(CONTENT_ID).entityId(ENTITY_ID) + .from(FROM_NUMBER).appHash(APP_HASH).build(); + case EMAIL -> new EmailWorkflow(TO_EMAIL, FROM_EMAIL); + default -> getWorkflowRequiredParamsForChannel(channel); + }; } Builder getBuilderRequiredParamsSingleWorkflow(Channel channel) { @@ -98,9 +85,14 @@ Builder getBuilderAllParamsSingleWorkflow(Channel channel) { } String getExpectedRequiredParamsForSingleWorkflowJson(Channel channel) { - String to = channel == Channel.EMAIL ? TO_EMAIL : TO_NUMBER; - return "{\"brand\":\""+BRAND+"\",\"workflow\":[{" + - "\"channel\":\""+channel+"\",\"to\":\""+to+"\"}]}"; + var to = channel == Channel.EMAIL ? TO_EMAIL : TO_NUMBER; + var json = "{\"brand\":\""+BRAND+"\",\"workflow\":[{" + + "\"channel\":\""+channel+"\",\"to\":\""+to+'"'; + if (channel == Channel.WHATSAPP || channel == Channel.WHATSAPP_INTERACTIVE) { + json += ",\"from\":\"" + FROM_NUMBER + '"'; + } + json += "}]}"; + return json; } String getExpectedAllParamsForSingleWorkflowJson(Channel channel) { @@ -112,7 +104,7 @@ String getExpectedAllParamsForSingleWorkflowJson(Channel channel) { "\"content_id\":\""+CONTENT_ID+"\",\"entity_id\":\""+ENTITY_ID+"\""; expectedJson = expectedJson.replace(prefix, replacement); } - if (channel == Channel.WHATSAPP || channel == Channel.SMS) { + if (channel == Channel.SMS) { prefix = TO_NUMBER + '"'; replacement = prefix + ",\"from\":\""+FROM_NUMBER+"\""; expectedJson = expectedJson.replace(prefix, replacement); @@ -188,8 +180,8 @@ public void testAllWorkflowsWithoutRecipient() { assertThrows(RuntimeException.class, () -> SilentAuthWorkflow.builder(invalid).build()); assertThrows(RuntimeException.class, () -> new SmsWorkflow(invalid)); assertThrows(RuntimeException.class, () -> new VoiceWorkflow(invalid)); - assertThrows(RuntimeException.class, () -> new WhatsappWorkflow(invalid)); - assertThrows(RuntimeException.class, () -> new WhatsappCodelessWorkflow(invalid)); + assertThrows(RuntimeException.class, () -> new WhatsappWorkflow(invalid, FROM_NUMBER)); + assertThrows(RuntimeException.class, () -> new WhatsappCodelessWorkflow(invalid, FROM_NUMBER)); assertThrows(RuntimeException.class, () -> new EmailWorkflow(invalid)); } } @@ -200,6 +192,7 @@ public void testSenderValidation() { assertEquals(FROM_EMAIL, new EmailWorkflow(TO_EMAIL, FROM_EMAIL).getFrom()); for (String invalid : new String[]{"", " "}) { assertThrows(IllegalArgumentException.class, () -> new WhatsappWorkflow(TO_NUMBER, invalid)); + assertThrows(IllegalArgumentException.class, () -> new WhatsappCodelessWorkflow(TO_NUMBER, invalid)); assertThrows(IllegalArgumentException.class, () -> new EmailWorkflow(TO_EMAIL, invalid)); } } @@ -208,7 +201,7 @@ public void testSenderValidation() { public void testCodelessAndCodeLengthValidation() { Builder requiredBuilder = getBuilderRequiredParamsSingleWorkflow(Channel.SILENT_AUTH); assertTrue(requiredBuilder.build().isCodeless()); - requiredBuilder.addWorkflow(new WhatsappCodelessWorkflow(TO_NUMBER)); + requiredBuilder.addWorkflow(new WhatsappCodelessWorkflow(TO_NUMBER, FROM_NUMBER)); assertTrue(requiredBuilder.build().isCodeless()); requiredBuilder.codeLength(9); assertThrows(IllegalStateException.class, requiredBuilder::build); @@ -220,7 +213,7 @@ public void testCodelessAndCodeLengthValidation() { public void testCodelessAndCodeValidation() { Builder requiredBuilder = getBuilderRequiredParamsSingleWorkflow(Channel.SILENT_AUTH); assertTrue(requiredBuilder.build().isCodeless()); - requiredBuilder.addWorkflow(new WhatsappCodelessWorkflow(TO_NUMBER)); + requiredBuilder.addWorkflow(new WhatsappCodelessWorkflow(TO_NUMBER, FROM_NUMBER)); assertTrue(requiredBuilder.build().isCodeless()); requiredBuilder.code("12345678"); assertThrows(IllegalStateException.class, requiredBuilder::build); @@ -289,7 +282,7 @@ public void testInvalidSmsAppHash() { String appHash = workflow.getAppHash(); assertNotNull(appHash); assertEquals(11, appHash.length()); - assertEquals(workflow, new SmsWorkflow(TO_NUMBER, appHash)); + assertEquals(workflow, SmsWorkflow.builder(TO_NUMBER).appHash(valid).build()); } @Test @@ -333,8 +326,8 @@ public void testSilentAuthMustBeFirstWorkflow() { VerificationRequest.Builder builder = VerificationRequest.builder().brand("Test"); assertThrows(IllegalStateException.class, builder::build); SilentAuthWorkflow saw = new SilentAuthWorkflow("447900000001"); - assertEquals(saw, builder.addWorkflow(saw).build().getWorkflows().get(0)); - WhatsappWorkflow waw = new WhatsappWorkflow(saw.getTo()); + assertEquals(saw, builder.addWorkflow(saw).build().getWorkflows().getFirst()); + WhatsappWorkflow waw = new WhatsappWorkflow(saw.getTo(), FROM_NUMBER); assertEquals(waw, builder.addWorkflow(waw).build().getWorkflows().get(1)); builder.workflows(Arrays.asList(waw, saw)); assertEquals(2, builder.workflows.size()); @@ -349,6 +342,19 @@ public void testInvalidLocale() throws Exception { assertNotNull(builder.locale("ab-cd").build().getLocale()); } + @Test + public void testWhatsappWorkflowsWithoutSender() { + var request = newBuilder() + .addWorkflow(new WhatsappWorkflow(TO_NUMBER)) + .addWorkflow(new WhatsappCodelessWorkflow(TO_NUMBER)) + .build(); + assertNotNull(request); + var expectedJson = "{\"brand\":\""+BRAND+"\",\"workflow\":[" + + "{\"channel\":\"whatsapp\",\"to\":\""+TO_NUMBER+"\"}," + + "{\"channel\":\"whatsapp_interactive\",\"to\":\""+TO_NUMBER+"\"}]}"; + assertEquals(expectedJson, request.toJson()); + } + @Test public void triggerJsonProcessingException() { class SelfRefrencing extends VerificationRequest { @@ -359,7 +365,7 @@ class SelfRefrencing extends VerificationRequest { } } assertThrows(VonageUnexpectedException.class, () -> new SelfRefrencing(VerificationRequest.builder() - .addWorkflow(new SmsWorkflow("447900000000")).brand("Test")).toJson() + .addWorkflow(new SmsWorkflow(TO_NUMBER)).brand("Test")).toJson() ); } } diff --git a/src/test/java/com/vonage/client/verify2/Verify2ClientTest.java b/src/test/java/com/vonage/client/verify2/Verify2ClientTest.java index cca9715c5..151fc36f7 100644 --- a/src/test/java/com/vonage/client/verify2/Verify2ClientTest.java +++ b/src/test/java/com/vonage/client/verify2/Verify2ClientTest.java @@ -37,12 +37,13 @@ public Verify2ClientTest() { } void assert429ResponseException(Executable invocation) throws Exception { - String response = "{\n" + - " \"title\": \"Rate Limit Hit\",\n" + - " \"type\": \"https://www.developer.vonage.com/api-errors#throttled\",\n" + - " \"detail\": \"Please wait, then retry your request\",\n" + - " \"instance\": \"bf0ca0bf927b3b52e3cb03217e1a1ddf\"\n" + - "}"; + String response = """ + { + "title": "Rate Limit Hit", + "type": "https://www.developer.vonage.com/api-errors#throttled", + "detail": "Please wait, then retry your request", + "instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf" + }"""; assertApiResponseException(429, response, VerifyResponseException.class, invocation); } @@ -55,7 +56,7 @@ VerificationRequest newVerificationRequestWithAllParamsAndWorkflows() { new EmailWorkflow(toEmail, fromEmail), new VoiceWorkflow(toNumber), new WhatsappWorkflow(toNumber, fromNumber), - new WhatsappCodelessWorkflow(toNumber) + new WhatsappCodelessWorkflow(toNumber, fromNumber) ); return VerificationRequest.builder() .brand("Nexmo").fraudCheck(false) @@ -107,7 +108,9 @@ protected String expectedEndpointUri(VerificationRequest request) { protected VerificationRequest sampleRequest() { return VerificationRequest.builder() .clientRef("my-personal-reference").locale("ar-XA") - .addWorkflow(new SmsWorkflow("447700900001", "447900000002", "FA+9qCX9VSu")) + .addWorkflow(SmsWorkflow.builder("447700900001") + .from("447900000002").appHash("FA+9qCX9VSu").build() + ) .brand("ACME, Inc").codeLength(6).channelTimeout(320).build(); }