Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 107 additions & 50 deletions src/main/Notification/WindmillNotificationClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,107 @@
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;

import static Security.EnvUtil.requireEnv;
import static Security.EnvUtil.requireEnvWithPattern;

@Slf4j
public class WindmillNotificationClient {
private final OkHttpClient client;
private final Gson gson;
private static final Pattern PHONE_PATTERN = Pattern.compile("\\+1\\d{10}"); // +1 followed by 10 digits
private static final Pattern EMAIL_PATTERN
= Pattern.compile("^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9]([a-zA-Z0-9.\\-]*[a-zA-Z0-9])?\\.[a-zA-Z]{2,}$");
private final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
private final Gson gson = new Gson();
private final String WINDMILL_URL;
private final String WINDMILL_TOKEN;
private final String TWILIO_PHONE_NUMBER;
private final String KEEPID_EMAIL_ADDRESS;
private final Map<String, String> twilioResource;
private final Pattern PHONE_PATTERN = Pattern.compile("\\+1\\d{10}"); // +1 followed by 10 digits
private final Map<String, String> smtpResource;

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);
WINDMILL_URL = requireEnv("WINDMILL_URL");
WINDMILL_TOKEN = requireEnv("WINDMILL_TOKEN");
TWILIO_PHONE_NUMBER = requireEnvWithPattern("TWILIO_PHONE_NUMBER", PHONE_PATTERN);
KEEPID_EMAIL_ADDRESS = requireEnvWithPattern("EMAIL_ADDRESS", EMAIL_PATTERN);

twilioResource = Map.of(
"accountSid", requireEnv("ACCOUNT_SID"),
"token", requireEnv("AUTH_TOKEN_TWILIO")
);

smtpResource = Map.of(
"host", requireEnv("EMAIL_HOST"),
"port", requireEnv("EMAIL_PORT"),
"user", KEEPID_EMAIL_ADDRESS,
"password", requireEnv("EMAIL_PASSWORD")
);
}

// Constructor for testing
public WindmillNotificationClient(String windmillUrl, String windmillToken, String twilioPhoneNumber,
String twilioAccountSid, String twilioAuthToken) {
this.client = new OkHttpClient();
this.gson = new Gson();
// constructor for testing
public WindmillNotificationClient(String windmillUrl, String windmillToken,
String twilioPhoneNumber, String twilioAccountSid, String twilioAuthToken,
String keepidEmailAddress, String emailHost, String emailPort, String emailPassword) {
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);
this.KEEPID_EMAIL_ADDRESS = keepidEmailAddress;

this.twilioResource = Map.of(
"accountSid", twilioAccountSid,
"token", twilioAuthToken
);

this.smtpResource = Map.of(
"host", emailHost,
"password", emailPassword,
"port", emailPort,
"user", keepidEmailAddress
);
}

public static boolean isValidPhoneNumber(String phoneNumber) {
return phoneNumber != null && PHONE_PATTERN.matcher(phoneNumber).matches();
}

public boolean isValidPhoneNumber(String phoneNumber) {
return phoneNumber != null && this.PHONE_PATTERN.matcher(phoneNumber).matches();
public static boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}

public void executeRequest(Request request, Callback callback) {
public void executeRequest(Request request) {
Callback callback = new Callback() {
public void onFailure(@NotNull Call call, @NotNull IOException e) {
log.error("executeRequest failed: " + e.getMessage());
}

public void onResponse(@NotNull Call call, @NotNull Response response) {
if (response.isSuccessful()) {
log.info("executeRequest completed successfully. Status: {}", response.code());
} else {
log.warn("executeRequest completed but failed. Status: {}", response.code());
}
}
};
client.newCall(request).enqueue(callback);
}

public void sendSms(String to, String message) {
if (!isValidPhoneNumber(to)) {
log.error("sendSms failed: invalid phone number provided: {}", to);
log.error("sendSms failed: invalid phone number provided");
return;
}
if (message == null || message.isBlank()) {
log.error("sendSms failed: empty message provided: {}", message);
log.error("sendSms failed: empty message provided");
return;
}

Expand All @@ -87,26 +125,45 @@ public void sendSms(String to, String message) {
.addHeader("Authorization", "Bearer " + this.WINDMILL_TOKEN)
.build();

log.info("Sending SMS to {} with message: {}", to, message);
log.info("Sending SMS notification request to windmill webhook endpoint");

Callback callback = new Callback() {
public void onFailure(@NotNull Call call, @NotNull IOException e) {
log.error("sendSms failed: " + e.getMessage());
}
executeRequest(request);
}

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);
public void sendEmail(String toEmailAddress, String message, String subjectLine) {
if (!isValidEmail(toEmailAddress)) {
log.error("sendEmail failed: invalid to email address provided");
return;
}
if (message == null || message.isBlank()) {
log.error("sendEmail failed: empty message provided");
return;
}
if (subjectLine == null || subjectLine.isBlank()) {
log.error("sendEmail failed: empty subject line provided");
return;
}

Map<String, Object> payload = Map.of(
"method", "email",
"message", message,
"email_config", Map.of(
"email_auth", smtpResource,
"to_email_address", toEmailAddress,
"from_email_address", this.KEEPID_EMAIL_ADDRESS,
"subject", subjectLine
),
"sms_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 email notification request to windmill webhook endpoint");

executeRequest(request);
}
}
24 changes: 24 additions & 0 deletions src/main/Security/EnvUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package Security;

import java.util.regex.Pattern;

public class EnvUtil {
public static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException("Missing required env var: " + name);
}
return value;
}

public static String requireEnvWithPattern(String name, Pattern pattern) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException("Missing required env var: " + name);
}
if (!pattern.matcher(value).matches()) {
throw new IllegalStateException("env var: " + name + " must be of pattern: " + pattern);
}
return value;
}
}
4 changes: 3 additions & 1 deletion src/test/NotificationTest/NotifyIdPickupServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public void setUp() {
activityDao = ActivityDaoFactory.create(DeploymentLevel.IN_MEMORY);
notificationClient =
new WindmillNotificationClient(
"http://localhost:9999", "fake-token", "+10000000000", "fake-sid", "fake-auth");
"http://localhost:9999", "fake-token",
"+10000000000", "fake-sid", "fake-auth",
"fake_email", "fake_host", "fake_port", "fake_password");
}

@After
Expand Down
72 changes: 56 additions & 16 deletions src/test/NotificationTest/WindmillNotificationClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import Notification.WindmillNotificationClient;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Callback;
import okhttp3.Request;
import org.junit.Test;

Expand All @@ -11,17 +10,14 @@

@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") {
var testClient = new WindmillNotificationClient(
"http://localhost", "test_windmill_token",
"test_twilio_phone_number", "test_twilio_account_sid", "test_twilio_auth_token",
"fake_email", "fake_host", "fake_port", "fake_password") {
@Override
public void executeRequest(Request request, Callback callback) {
public void executeRequest(Request request) {
// Don't actually send, just verify the request looks right
assertNotNull(request);
assertEquals("POST", request.method());
Expand All @@ -31,18 +27,62 @@ public void executeRequest(Request request, Callback callback) {
assertDoesNotThrow(() -> testClient.sendSms("+12025551234", "Test"));
}

@Test
public void sendEmailSuccess() {
var testClient = new WindmillNotificationClient(
"http://localhost", "test_windmill_token",
"test_twilio_phone_number", "test_twilio_account_sid", "test_twilio_auth_token",
"fake_email", "fake_host", "fake_port", "fake_password") {
@Override
public void executeRequest(Request request) {
// Don't actually send, just verify the request looks right
assertNotNull(request);
assertEquals("POST", request.method());
}
};

assertDoesNotThrow(() -> testClient.sendEmail("foo@example.com", "Test", "Test"));
}

@Test
public void testValidPhoneNumbers() {
assertTrue(client.isValidPhoneNumber("+12025551234"));
assertTrue(client.isValidPhoneNumber("+19999999999"));
assertTrue(WindmillNotificationClient.isValidPhoneNumber("+12025551234"));
assertTrue(WindmillNotificationClient.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));
assertFalse(WindmillNotificationClient.isValidPhoneNumber("12025551234")); // missing +
assertFalse(WindmillNotificationClient.isValidPhoneNumber("+44123456789")); // wrong country code
assertFalse(WindmillNotificationClient.isValidPhoneNumber("+1202555123")); // too few digits
assertFalse(WindmillNotificationClient.isValidPhoneNumber("+120255512345")); // too many digits
assertFalse(WindmillNotificationClient.isValidPhoneNumber(null));
}

@Test
public void testValidEmails() {
assertTrue(WindmillNotificationClient.isValidEmail("user@example.com"));
assertTrue(WindmillNotificationClient.isValidEmail("user.name@example.com"));
assertTrue(WindmillNotificationClient.isValidEmail("user+tag@example.com"));
assertTrue(WindmillNotificationClient.isValidEmail("user_name@example.org"));
assertTrue(WindmillNotificationClient.isValidEmail("user123@sub.domain.com"));
assertTrue(WindmillNotificationClient.isValidEmail("u@example.io"));
assertTrue(WindmillNotificationClient.isValidEmail("test@my-domain.co.uk"));
}

@Test
public void testInvalidEmails() {
assertFalse(WindmillNotificationClient.isValidEmail("user@"));
assertFalse(WindmillNotificationClient.isValidEmail("@example.com"));
assertFalse(WindmillNotificationClient.isValidEmail("userexample.com"));
assertFalse(WindmillNotificationClient.isValidEmail("user@@example.com"));
assertFalse(WindmillNotificationClient.isValidEmail("us er@example.com"));
assertFalse(WindmillNotificationClient.isValidEmail("user@exa mple.com"));
assertFalse(WindmillNotificationClient.isValidEmail("user@.com"));
assertFalse(WindmillNotificationClient.isValidEmail("user@-example.com"));
assertFalse(WindmillNotificationClient.isValidEmail("user@example.c"));
assertFalse(WindmillNotificationClient.isValidEmail("user@example"));
assertFalse(WindmillNotificationClient.isValidEmail(""));
assertFalse(WindmillNotificationClient.isValidEmail(null));
}
}