diff --git a/src/main/Notification/WindmillNotificationClient.java b/src/main/Notification/WindmillNotificationClient.java new file mode 100644 index 00000000..aeb9182f --- /dev/null +++ b/src/main/Notification/WindmillNotificationClient.java @@ -0,0 +1,112 @@ +package Notification; + +import okhttp3.*; +import com.google.gson.Gson; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + +@Slf4j +public class WindmillNotificationClient { + private final OkHttpClient client; + private final Gson gson; + private final String WINDMILL_URL; + private final String WINDMILL_TOKEN; + private final String TWILIO_PHONE_NUMBER; + private final Map twilioResource; + private final Pattern PHONE_PATTERN = Pattern.compile("\\+1\\d{10}"); // +1 followed by 10 digits + + public WindmillNotificationClient() { + this.client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + this.gson = new Gson(); + this.WINDMILL_URL = System.getenv("WINDMILL_URL"); + this.WINDMILL_TOKEN = System.getenv("WINDMILL_TOKEN"); + this.TWILIO_PHONE_NUMBER = System.getenv("TWILIO_PHONE_NUMBER"); + this.twilioResource = new HashMap<>(); + String TWILIO_ACCOUNT_SID = System.getenv("ACCOUNT_SID"); + String TWILIO_AUTH_TOKEN = System.getenv("AUTH_TOKEN_TWILIO"); + this.twilioResource.put("accountSid", TWILIO_ACCOUNT_SID); + this.twilioResource.put("token", TWILIO_AUTH_TOKEN); + } + + // Constructor for testing + public WindmillNotificationClient(String windmillUrl, String windmillToken, String twilioPhoneNumber, + String twilioAccountSid, String twilioAuthToken) { + this.client = new OkHttpClient(); + this.gson = new Gson(); + this.WINDMILL_URL = windmillUrl; + this.WINDMILL_TOKEN = windmillToken; + this.TWILIO_PHONE_NUMBER = twilioPhoneNumber; + this.twilioResource = new HashMap<>(); + this.twilioResource.put("accountSid", twilioAccountSid); + this.twilioResource.put("token", twilioAuthToken); + } + + public boolean isValidPhoneNumber(String phoneNumber) { + return phoneNumber != null && this.PHONE_PATTERN.matcher(phoneNumber).matches(); + } + + public void executeRequest(Request request, Callback callback) { + client.newCall(request).enqueue(callback); + } + + public void sendSms(String to, String message) { + if (!isValidPhoneNumber(to)) { + log.error("sendSms failed: invalid phone number provided: {}", to); + return; + } + if (message == null || message.isBlank()) { + log.error("sendSms failed: empty message provided: {}", message); + return; + } + + Map payload = Map.of( + "method", "sms", + "message", message, + "sms_config", Map.of( + "twilio_auth", twilioResource, + "to_phone_number", to, + "from_phone_number", this.TWILIO_PHONE_NUMBER + ), + "email_config", Map.of() + ); + + Request request = new Request.Builder() + .url(this.WINDMILL_URL) + .post(RequestBody.create(gson.toJson(payload), MediaType.parse("application/json"))) + .addHeader("Authorization", "Bearer " + this.WINDMILL_TOKEN) + .build(); + + log.info("Sending SMS to {} with message: {}", to, message); + + Callback callback = new Callback() { + public void onFailure(@NotNull Call call, @NotNull IOException e) { + log.error("sendSms failed: " + e.getMessage()); + } + + public void onResponse(@NotNull Call call, @NotNull Response response) { + try (response) { + if (response.isSuccessful()) { + log.info("sent SMS successfully. Status: {}", response.code()); + } else { + log.warn("SMS request completed but failed. Status: {}, Body: {}", + response.code(), response.body() != null ? response.body().string() : ""); + } + } catch (IOException e) { + log.error("caught error reading SMS response: " + e.getMessage()); + } + } + }; + executeRequest(request, callback); + } +} diff --git a/src/test/NotificationTest/WindmillNotificationClientTest.java b/src/test/NotificationTest/WindmillNotificationClientTest.java new file mode 100644 index 00000000..ee9ebb87 --- /dev/null +++ b/src/test/NotificationTest/WindmillNotificationClientTest.java @@ -0,0 +1,48 @@ +package NotificationTest; + +import Notification.WindmillNotificationClient; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Callback; +import okhttp3.Request; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@Slf4j +public class WindmillNotificationClientTest { + + WindmillNotificationClient client = new WindmillNotificationClient("http://localhost", + "test_windmill_token", "test_twilio_phone_number", + "test_twilio_account_sid", "test_twilio_auth_token"); + @Test + public void sendSMSSuccess() { + var testClient = new WindmillNotificationClient("http://localhost", + "test_windmill_token", "test_twilio_phone_number", + "test_twilio_account_sid", "test_twilio_auth_token") { + @Override + public void executeRequest(Request request, Callback callback) { + // Don't actually send, just verify the request looks right + assertNotNull(request); + assertEquals("POST", request.method()); + } + }; + + assertDoesNotThrow(() -> testClient.sendSms("+12025551234", "Test")); + } + + @Test + public void testValidPhoneNumbers() { + assertTrue(client.isValidPhoneNumber("+12025551234")); + assertTrue(client.isValidPhoneNumber("+19999999999")); + } + + @Test + public void testInvalidPhoneNumbers() { + assertFalse(client.isValidPhoneNumber("12025551234")); // missing + + assertFalse(client.isValidPhoneNumber("+44123456789")); // wrong country code + assertFalse(client.isValidPhoneNumber("+1202555123")); // too few digits + assertFalse(client.isValidPhoneNumber("+120255512345")); // too many digits + assertFalse(client.isValidPhoneNumber(null)); + } +} diff --git a/src/test/NotificationTest/WindmillWebhookManualTest.java b/src/test/NotificationTest/WindmillWebhookManualTest.java new file mode 100644 index 00000000..6caf821c --- /dev/null +++ b/src/test/NotificationTest/WindmillWebhookManualTest.java @@ -0,0 +1,25 @@ +package NotificationTest; + +import Notification.WindmillNotificationClient; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +// Sends actual webhooks to windmill +// Need to set environment variables in run configuration in Intellij (look at WindmillNotificationClient.java) +class WindmillWebhookManualTest { + + @Test + void sendSms() throws InterruptedException { + WindmillNotificationClient client = new WindmillNotificationClient(); + + System.out.println("Sending webhook..."); + assertDoesNotThrow(() -> { + client.sendSms("TEST_PHONE_NUMBER_HERE", "Test payload"); + }); + System.out.println("Done! Check the webhook dashboard to verify receipt."); + + // wait for callback + Thread.sleep(10000); + } +}