diff --git a/.gitignore b/.gitignore index 6143e53..54e422a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -# Compiled class file +target *.class +# intellij +*.iml +.idea + # Log file *.log @@ -20,3 +24,4 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + diff --git a/README.md b/README.md index 2daf92f..3928a7f 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ -# mailing-list-client \ No newline at end of file +[`mailing-list-client`](mailing-list-client/README.md) - This project +provides an API for integrating with a mailing list service. It provides +an implementation of the API for Mailchimp. + +[`mailchimp-webhooks`](mailchimp-webhooks/README.md) - This project +provides an small example Spring Boot client that can listen to webhooks +from Mailchimp that register user details changing in Mailchimp. + diff --git a/mailchimp-webhooks/README.md b/mailchimp-webhooks/README.md new file mode 100644 index 0000000..828a218 --- /dev/null +++ b/mailchimp-webhooks/README.md @@ -0,0 +1,16 @@ +## Mailchimp Webhook Service + +A proof of concept Spring Boot service used create a simple webhook +service for MailChimp. Currently accepts only "changed email" hooks. + +On recieving a webhook, it will write the `data[old_email]` and the +`data[new_url]` to the logs. + +### Configuration + +Requires `${mailing.webhook.key}` to be set in the Spring +`application.properties`, [providing some security to the +application](https://developer.mailchimp.com/documentation/mailchimp/guides/about-webhooks/#securing-webhooks). + +The URL for the webhook will then be `${base.uri}/${mailing.webhook.key}`. + diff --git a/mailchimp-webhooks/pom.xml b/mailchimp-webhooks/pom.xml new file mode 100644 index 0000000..ca49737 --- /dev/null +++ b/mailchimp-webhooks/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + uk.ac.stfc.facilities + mailing + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 1.5.4.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.apache.logging.log4j + log4j-api + 2.8.2 + + + org.apache.logging.log4j + log4j-core + 2.8.2 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/mailchimp-webhooks/src/main/java/uk/ac/stfc/facilities/mailing/spring/Application.java b/mailchimp-webhooks/src/main/java/uk/ac/stfc/facilities/mailing/spring/Application.java new file mode 100644 index 0000000..cb9c4c8 --- /dev/null +++ b/mailchimp-webhooks/src/main/java/uk/ac/stfc/facilities/mailing/spring/Application.java @@ -0,0 +1,12 @@ +package uk.ac.stfc.facilities.mailing.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/mailchimp-webhooks/src/main/java/uk/ac/stfc/facilities/mailing/spring/MailchimpWebhookController.java b/mailchimp-webhooks/src/main/java/uk/ac/stfc/facilities/mailing/spring/MailchimpWebhookController.java new file mode 100644 index 0000000..2dd7200 --- /dev/null +++ b/mailchimp-webhooks/src/main/java/uk/ac/stfc/facilities/mailing/spring/MailchimpWebhookController.java @@ -0,0 +1,43 @@ +package uk.ac.stfc.facilities.mailing.spring; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +/** + * A Spring controller that listens to webhooks from Mailchimp, it + * currently handles the webhook handshake and updates to the users + * email. It refers to the mailing.webhook.key key in + * config to create the mapping. + */ +@Controller +@RequestMapping("/${mailing.webhook.key}") +public class MailchimpWebhookController { + + private static final Logger LOGGER = LogManager.getLogger(); + + /** + * A simple method to respond with a successful status which is + * required for Mailchimp to accept a webhook client. + */ + @GetMapping + @ResponseStatus(value = HttpStatus.OK) + public void handshake() { + } + + /** + * A webhook that listens to changes to a user's email + * address change. + * @param oldEmail the old email for the user + * @param newEmail the new email for the user + */ + @PostMapping + @ResponseBody + public void hook( + @RequestParam("data[old_email]") String oldEmail, + @RequestParam("data[new_email]") String newEmail + ) { + LOGGER.info("old: " + oldEmail + " to " + newEmail); + } +} diff --git a/mailchimp-webhooks/src/main/resources/application.properties b/mailchimp-webhooks/src/main/resources/application.properties new file mode 100644 index 0000000..24eab5e --- /dev/null +++ b/mailchimp-webhooks/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# The mailing.key is used to create a more secure webhook URL, see: +# https://developer.mailchimp.com/documentation/mailchimp/guides/about-webhooks/#securing-webhooks +# This will mean the Webhook URL will look like "${base.uri}/${mailing.key}" +mailing.webhook.key= + diff --git a/mailing-list-client/README.md b/mailing-list-client/README.md new file mode 100644 index 0000000..b994112 --- /dev/null +++ b/mailing-list-client/README.md @@ -0,0 +1,31 @@ +## `mailing-list-api` + +Contains an API for connecting to mailing lists - +`uk.ac.stfc.facilities.mailing.api`. + +Contains an implementation of the mailing lists API for Mailchimp - +`uk.ac.stfc.facilities.mailing.mailchimp` + +### Using the `MailchimpClient` + +1. Create an instance of the `MailchimpClientConfiguration`, with the API key +given as an argument to the constructor. +```java +MailchimpClientConfiguration config = new MailchimpClientConfiguration(API_KEY); +``` +2. Get a new instance of the `MailchimpClient` using the factory method. This +takes the configuration in as an argument. +```java +MailingListClient client = MailchimpClient.getInstance(config); +``` + +The client is now ready to use. + +### Running the integration tests for the `MailchimpClient`. + +An [`applications.test.properties`](mailing-list-api/application.test.properties) +file must exist for the mailchimp client. + +**NB:** please ensure these tests are ran against an account with no +billing information attatched to it. + diff --git a/mailing-list-client/application.test.properties b/mailing-list-client/application.test.properties new file mode 100644 index 0000000..d028f91 --- /dev/null +++ b/mailing-list-client/application.test.properties @@ -0,0 +1,17 @@ +# The API key for the Mailchimp. This can be found at: +# https://admin.mailchimp.com/account/api/ +mailchimp.api.key= + +# The ID of an existing list in Mailchimp e.g. abcde1234 +mailchimp.list.id= +# The name of the existing list in Mailchimp e.g. My Mailing List +mailchimp.list.name= + +# The test domain that users belong to e.g. test.your-domain.com +# This cannot be a domain like example.com because Mailchimp checks +# for fake looking emails. Three emails should be added to the +# existing mailing list with the following statuses: +# - permanent.test.1@ - subscribed +# - permanent.test.2@ - subscribed +# - permanent.test.3@ - subscribed +mailchimp.test.domain= diff --git a/mailing-list-client/pom.xml b/mailing-list-client/pom.xml new file mode 100644 index 0000000..5234f1f --- /dev/null +++ b/mailing-list-client/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + uk.ac.stfc.facilities + mailing-list-client + 0.0.1-SNAPSHOT + + + 1.8 + 1.8 + 1.8 + + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + com.google.code.gson + gson + 2.6.2 + + + org.apache.logging.log4j + log4j-core + 2.8.2 + + + org.apache.logging.log4j + log4j-api + 2.8.2 + + + + org.junit.jupiter + junit-jupiter-api + 5.0.0-RC2 + test + + + org.junit.jupiter + junit-jupiter-params + 5.0.0-RC2 + test + + + + org.assertj + assertj-core + 3.8.0 + test + + + org.mockito + mockito-core + 2.8.47 + test + + + + + + + maven-surefire-plugin + 2.19.1 + + + org.junit.platform + junit-platform-surefire-provider + 1.0.0-RC2 + + + org.junit.jupiter + junit-jupiter-engine + 5.0.0-RC2 + + + + + + + diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/MailingListClient.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/MailingListClient.java new file mode 100644 index 0000000..d6a8184 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/MailingListClient.java @@ -0,0 +1,294 @@ +package uk.ac.stfc.facilities.mailing.api; + +import uk.ac.stfc.facilities.mailing.api.data.*; +import uk.ac.stfc.facilities.mailing.api.exceptions.MailingListClientException; +import uk.ac.stfc.facilities.mailing.api.exceptions.NotFoundMailingListClientException; + +import java.util.Set; + +/** + * A client for mailing lists service with the following functionality: + *
    + *
  • retrieving lists & list members
  • + *
  • managing members' subscriptions to lists
  • + *
  • managing list segments & list segment members
  • + *
+ */ +public interface MailingListClient { + + /** + * Retrieves all lists. + * + * @return the retrieved lists + * + * @throws MailingListClientException if the resource is unavailable + */ + MailingListDescriptors getAllListDescriptors() + throws MailingListClientException; + + /** + * Retrieves the list with the given list ID. + * + * @param listId the ID of the list to retrieve + * + * @return the list with the given list ID + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListDescriptor getListDescriptor(String listId) + throws MailingListClientException; + + /** + * Retrieves all members of the list by the given list ID. + * + * @param listId the ID of the list from which the members are + * retrieved + * + * @return all members of the list by the given list ID + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListMembers getMembersByList(String listId) + throws MailingListClientException; + + /** + * Retrieves a member by their email. + * + * @param listId the ID of the list from which the members is + * retrieved + * @param email the email of the member to retrieve + * + * @return the member of the list with the given email + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the ID does + * not exist or the member of the + * list does not exist + */ + MailingListMember getMemberOfList(String listId, String email) + throws MailingListClientException; + + /** + * Subscribes the member of the given email to list with the given + * list ID. If the member has already specified their subscription + * status this will not change the status but the request will be + * successful. The response will show the actual status of the + * member. + * + * @param listId the ID of the list to which the member will be + * subscribed + * @param email the email of the member which will be subscribed + * + * @return the member of the list for which the subscription was + * requested + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListMember subscribeMember(String listId, String email) + throws MailingListClientException; + + /** + * Unsubscribes the member of the given email from the list with + * the given list ID. If the member has already specified their + * subscription status, this will not change the status but the + * request will be successful. The response will show the actual + * status of the member. + * + * @param listId the ID of the list from which the member will be + * unsubscribed + * @param email the email of the member which will be unsubscribed + * + * @return the member of the list for which the unsubscription was + * requested + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListMember unsubscribeMember(String listId, String email) + throws MailingListClientException; + + /** + * Forces the subscription of the member with the given email + * from the list with the given list ID. This overrides any + * previous preference the member has. + * + * @param listId the ID of the list to which the member will be + * subscribed + * @param email the email of the member which will be subscribed + * + * @return the member of the list for which the subcription was + * requested + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListMember forceSubscribeMember(String listId, String email) + throws MailingListClientException; + + /** + * Forces the unsubscription of the member with the given email + * from the list with the given list ID. This overrides any + * previous preference the member has. + * + * @param listId the ID of the list from which the member will + * be unsubcribed + * @param email the email of the member which will be unsubscribed + * + * @return the member of the list for which the unsubscription was + * requested + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListMember forceUnsubscribeMember(String listId, String email) + throws MailingListClientException; + + /** + * Removes the member with the given email from the list entirely, + * this will remove any additional data stored about the member + * in the mailing list service. + * + * @param listId the ID of the list from which the member will be + * removed from + * @param email the email of the member who will be removed + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist or the member + * of the list does not exist + */ + void deleteMemberFromList(String listId, String email) + throws MailingListClientException; + + /** + * Retrieves the segments of the list with the given list ID. + * + * @param listId the ID of the list from which to retrieve the + * segment + * + * @return the segments of the list with the given ID + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListSegmentDescriptors getSegmentDescriptors(String listId) + throws MailingListClientException; + + /** + * Retrieves the segment of the list with the given list ID. + * + * @param listId the ID of the list from which to retrieve the + * segment + * @param segmentId the ID of the segment to retrieve + * + * @return the segment of the list with the given ID + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist or if the + * segment with the given ID does + * not exist + */ + MailingListSegmentDescriptor getSegmentDescriptor(String listId, String segmentId) + throws MailingListClientException; + + /** + * Retrieves the members of the segment with the given segment ID + * within the list with the given list ID. + * + * @param listId the ID of the list from which to retrieve the + * segment members + * @param segmentId the ID of the segment to retrieve the segment + * members + * + * @return the members for the segment with the given ID + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist or if the + * segment with the given ID does + * not exist + */ + MailingListMembers getMembersOfSegment(String listId, String segmentId) + throws MailingListClientException; + + /** + * Adds the members who have the given email addresses from the + * specified segment in the specified list. If the member does + * not exist in the list they will not be added to the list. + * + * @param listId the ID of the list in which the segment is + * @param segmentId the ID of the segment to add the members to + * @param emails the emails of the members to add + * + * @return the successfully added members + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist or if the + * segment with the given ID does + * not exist + */ + MailingListSegmentMemberChanges addMembersToSegment(String listId, String segmentId, Set emails) + throws MailingListClientException; + + /** + * Removes the members who have the given email addresses to the + * specified segment in the specified list. + * + * @param listId the ID of the list in which the segment is + * @param segmentId the ID of the segment to remove the members from + * @param emails the emails of the members to remove + * + * @return the successfully removed members + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with given ID does + * not exist or if the segment + * with the given ID does not + * exist + */ + MailingListSegmentMemberChanges removeMembersFromSegment(String listId, String segmentId, Set emails) + throws MailingListClientException; + + /** + * Creates a segment with the given name in the given list. + * + * @param listId the ID of the list in which to create the + * segment + * @param segmentName the name of the segment + * + * @return the descriptor of the segment + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist + */ + MailingListSegmentDescriptor createSegment(String listId, String segmentName) + throws MailingListClientException; + + /** + * Deletes the segment with the given ID in the given list. + * + * @param listId this ID of the list in which to create the segment + * @param segmentId the ID of the segment + * + * @throws MailingListClientException if the resource is unavailable + * @throws NotFoundMailingListClientException if the list with the given ID + * does not exist or if the + * segment with the given ID does + * not exist + */ + void deleteSegment(String listId, String segmentId) + throws MailingListClientException; +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListDescriptor.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListDescriptor.java new file mode 100644 index 0000000..4e23198 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListDescriptor.java @@ -0,0 +1,17 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +/** + * Describes a mailing list. + */ +public interface MailingListDescriptor { + + /** + * @return the ID of the mailing list + */ + String getId(); + + /** + * @return the name of the mailing list + */ + String getName(); +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListDescriptors.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListDescriptors.java new file mode 100644 index 0000000..b138bb5 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListDescriptors.java @@ -0,0 +1,14 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +import java.util.Set; + +/** + * Contains a set of mailing list descriptors. + */ +public interface MailingListDescriptors { + + /** + * @return the set of mailing list descriptors + */ + Set getLists(); +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListMember.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListMember.java new file mode 100644 index 0000000..cd9ba1c --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListMember.java @@ -0,0 +1,32 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +/** + * Describes a member of a mailing list. + */ +public interface MailingListMember { + + /** + * @return the ID of the mailing list member + */ + String getId(); + + /** + * @return the email address of the list member + */ + String getEmailAddress(); + + /** + * @return a global identifier for the member + */ + String getUniqueEmailId(); + + /** + * @return the status of the list member + */ + SubscriptionStatus getSubscriptionStatus(); + + /** + * @return the ID of the list + */ + String getListId(); +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListMembers.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListMembers.java new file mode 100644 index 0000000..2301a4c --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListMembers.java @@ -0,0 +1,14 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +import java.util.Set; + +/** + * Contains a set of mailing list members. + */ +public interface MailingListMembers { + + /** + * @return the set of members + */ + Set getMembers(); +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentDescriptor.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentDescriptor.java new file mode 100644 index 0000000..627c755 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentDescriptor.java @@ -0,0 +1,18 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +/** + * Describes a mailing list segment. + */ +public interface MailingListSegmentDescriptor { + + /** + * @return the ID of the segment + */ + String getId(); + + /** + * @return the name of the segment + */ + String getName(); + +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentDescriptors.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentDescriptors.java new file mode 100644 index 0000000..fa5f6a0 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentDescriptors.java @@ -0,0 +1,15 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +import java.util.Set; + +/** + * Contains a set of mailing list segment descriptors. + */ +public interface MailingListSegmentDescriptors { + + /** + * @return the set of mailing list segment descriptors + */ + Set getSegmentDescriptors(); + +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentMemberChanges.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentMemberChanges.java new file mode 100644 index 0000000..7090c87 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/MailingListSegmentMemberChanges.java @@ -0,0 +1,20 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +import java.util.Set; + +/** + * Describes the changes made to the segment members + */ +public interface MailingListSegmentMemberChanges { + + /** + * @return the set of members added to the segment + */ + Set getAddedMembers(); + + /** + * @return the set of members removed from the segment + */ + Set getRemovedMembers(); + +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/SubscriptionStatus.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/SubscriptionStatus.java new file mode 100644 index 0000000..1d04d5f --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/data/SubscriptionStatus.java @@ -0,0 +1,22 @@ +package uk.ac.stfc.facilities.mailing.api.data; + +/** + * The subscription status of a member. + */ +public enum SubscriptionStatus { + + /** + * The status when a member is subscribed. + */ + SUBSCRIBED, + + /** + * The status when a member is unsubscribed. + */ + UNSUBSCRIBED, + + /** + * The status when the state of subscription for a member is unknown. + */ + UNKNOWN +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/exceptions/MailingListClientException.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/exceptions/MailingListClientException.java new file mode 100644 index 0000000..b20b317 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/exceptions/MailingListClientException.java @@ -0,0 +1,16 @@ +package uk.ac.stfc.facilities.mailing.api.exceptions; + +/** + * Describes errors that occurred trying to get a resource from the + * mailing list client. + */ +public class MailingListClientException extends Exception { + + public MailingListClientException(String s) { + super(s); + } + + public MailingListClientException(String s, Throwable e) { + super(s, e); + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/exceptions/NotFoundMailingListClientException.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/exceptions/NotFoundMailingListClientException.java new file mode 100644 index 0000000..c6e195e --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/api/exceptions/NotFoundMailingListClientException.java @@ -0,0 +1,15 @@ +package uk.ac.stfc.facilities.mailing.api.exceptions; + +/** + * A MailingListClientException that describes the situation where a + * resource was not found. + */ +public class NotFoundMailingListClientException extends MailingListClientException { + public NotFoundMailingListClientException(String s) { + super(s); + } + + public NotFoundMailingListClientException(String s, Throwable e) { + super(s, e); + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClient.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClient.java new file mode 100644 index 0000000..b3c4225 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClient.java @@ -0,0 +1,164 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import org.apache.commons.codec.digest.DigestUtils; +import uk.ac.stfc.facilities.mailing.api.MailingListClient; +import uk.ac.stfc.facilities.mailing.api.data.*; +import uk.ac.stfc.facilities.mailing.api.exceptions.MailingListClientException; + +import java.util.Set; + +/** + * Implementation of MailingListClient that retrieves + * mailing list data from Mailchimp. It is best to have only one + * instance of this client per application. The implementation is + * thread safe. + *

+ * Creating a mailchimp client, where API_KEY is + * the Mailchimp API key: + *

+ * MailchimpClientConfiguration config = new MailchimpClientConfiguration(API_KEY);
+ * MailingListClient client = MailchimpClient.getInstance(configuration);
+ *   
+ *

+ */ +public class MailchimpClient implements MailingListClient { + + /** + * Generates an instance of the MailchimpClient with the given + * configuration. + * + * @param configuration the Mailchimp client configuration + * @return a new instance of the Mailchimp client + */ + public static MailchimpClient getInstance(MailchimpClientConfiguration configuration) { + return new MailchimpClient(MailchimpInternalHttpClient.getInstance(configuration)); + } + + private static String convertEmailToId(String email) { + return DigestUtils.md5Hex(email.toLowerCase()); + } + + private final MailchimpInternalHttpClient httpClient; + + private MailchimpClient(MailchimpInternalHttpClient internalHttpClient) { + this.httpClient = internalHttpClient; + } + + @Override + public MailingListDescriptors getAllListDescriptors() throws MailingListClientException { + return httpClient.get( + "lists", + MailchimpListDescriptors.class); + } + + @Override + public MailingListDescriptor getListDescriptor(String listId) throws MailingListClientException { + return httpClient.get( + "lists/" + listId, + MailchimpListDescriptor.class); + } + + @Override + public MailingListMembers getMembersByList(String listId) throws MailingListClientException { + return httpClient.get( + "lists/" + listId + "/members", + MailchimpListMembers.class); + } + + @Override + public MailingListMember getMemberOfList(String listId, String email) throws MailingListClientException { + return httpClient.get( + "lists/" + listId + "/members/" + convertEmailToId(email), + MailchimpListMember.class); + } + + @Override + public MailingListMember subscribeMember(String listId, String email) throws MailingListClientException { + + return httpClient.put("lists/" + listId + "/members/" + convertEmailToId(email), + MailchimpListMemberRequest.whichSubscribes(email), + MailchimpListMember.class); + } + + @Override + public MailingListMember unsubscribeMember(String listId, String email) throws MailingListClientException { + + return httpClient.put("lists/" + listId + "/members/" + convertEmailToId(email), + MailchimpListMemberRequest.whichUnsubscribes(email), + MailchimpListMember.class); + } + + @Override + public MailingListMember forceSubscribeMember(String listId, String email) throws MailingListClientException { + + return httpClient.put("lists/" + listId + "/members/" + convertEmailToId(email), + MailchimpListMemberRequest.whichForceSubscribes(email), + MailchimpListMember.class); + } + + @Override + public MailingListMember forceUnsubscribeMember(String listId, String email) throws MailingListClientException { + + return httpClient.put("lists/" + listId + "/members/" + convertEmailToId(email), + MailchimpListMemberRequest.whichForceUnsubscribes(email), + MailchimpListMember.class); + } + + @Override + public void deleteMemberFromList(String listId, String email) throws MailingListClientException { + httpClient.delete("lists/" + listId + "/members/" + convertEmailToId(email), + Void.class); + } + + @Override + public MailingListSegmentDescriptors getSegmentDescriptors(String listId) throws MailingListClientException { + return httpClient.get( + "lists/" + listId + "/segments", + MailchimpSegmentDescriptors.class); + } + + @Override + public MailingListSegmentDescriptor getSegmentDescriptor(String listId, String segmentId) throws MailingListClientException { + return httpClient.get( + "lists/" + listId + "/segments/" + segmentId, + MailchimpSegmentDescriptor.class); + } + + @Override + public MailingListMembers getMembersOfSegment(String listId, String segmentId) throws MailingListClientException { + return httpClient.get( + "lists/" + listId + "/segments/" + segmentId + "/members", + MailchimpListMembers.class); + } + + @Override + public MailingListSegmentMemberChanges addMembersToSegment(String listId, String segmentId, Set emails) throws MailingListClientException { + return httpClient.post( + "lists/" + listId + "/segments/" + segmentId, + MailchimpSegmentMemberRequest.whichAddsMembers(emails), + MailchimpSegmentMemberChanges.class + ); + } + + @Override + public MailingListSegmentMemberChanges removeMembersFromSegment(String listId, String segmentId, Set emails) throws MailingListClientException { + return httpClient.post( + "lists/" + listId + "/segments/" + segmentId, + MailchimpSegmentMemberRequest.whichRemovesMembers(emails), + MailchimpSegmentMemberChanges.class + ); + } + + @Override + public MailingListSegmentDescriptor createSegment(String listId, String segmentName) throws MailingListClientException { + return httpClient.post("lists/" + listId + "/segments", + MailchimpSegmentCreateRequest.whichCreatesASegment(segmentName), + MailchimpSegmentDescriptor.class); + } + + @Override + public void deleteSegment(String listId, String segmentId) throws MailingListClientException { + httpClient.delete("lists/" + listId + "/segments/" + segmentId, + Void.class); + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClientConfiguration.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClientConfiguration.java new file mode 100644 index 0000000..bf197ce --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClientConfiguration.java @@ -0,0 +1,50 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +/** + * Configuration for the Mailchimp client. + */ +public class MailchimpClientConfiguration { + private static final String SCHEME = "https"; + private static final int PORT = 443; + private static final String MAILCHIMP_API_BASE_HOST = "api.mailchimp.com"; + private static final String MAILCHIMP_API_VERSION_URI = "/3.0/"; + private static final String API_KEY_SPLITTER = "-"; + + private final String apiKey; + + private final String baseUri; + private final String host; + + /** + * Creates the configuration from the Mailchimp API key which + * contains all of the connection information for Mailchimp. + * + * @param apiKey the API key for Mailchimp. + */ + public MailchimpClientConfiguration(String apiKey) { + this.apiKey = apiKey; + String dataCenter = apiKey.split(API_KEY_SPLITTER)[1]; + this.host = dataCenter + "." + MAILCHIMP_API_BASE_HOST; + this.baseUri = SCHEME + "://" + host + MAILCHIMP_API_VERSION_URI; + } + + public String getApiKey() { + return apiKey; + } + + public String getBaseUrl() { + return baseUri; + } + + public String getHost() { + return host; + } + + public String getScheme() { + return SCHEME; + } + + public int getPort() { + return PORT; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpInternalHttpClient.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpInternalHttpClient.java new file mode 100644 index 0000000..d4b5811 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpInternalHttpClient.java @@ -0,0 +1,273 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.*; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import uk.ac.stfc.facilities.mailing.api.exceptions.MailingListClientException; +import uk.ac.stfc.facilities.mailing.api.exceptions.NotFoundMailingListClientException; +import uk.ac.stfc.facilities.mailing.mailchimp.gson.adapters.LocalDateTimeAdapter; + +import java.io.IOException; +import java.time.LocalDateTime; + +/** + * An internal client that handles the requests to Mailchimp, dealing + * with the conversion to and from the requests and responses from + * the DTOs. + */ +class MailchimpInternalHttpClient { + + /** + * The Mailchimp username for basic authentication doesn't matter, + * as long as some value is provided, it will be accepted. In this + * situation, “user” has been used as a placeholder. + */ + private static final String MAILCHIMP_USERNAME_PLACEHOLDER = "user"; + + private static final Logger LOG = LogManager.getLogger(); + + /** + * Used to create an instance of the internal client. + * + * @param configuration the configuration for the Mailchimp client. + * @return a new instance of the internal HTTP client + */ + static MailchimpInternalHttpClient getInstance(MailchimpClientConfiguration configuration) { + + CredentialsProvider provider = new BasicCredentialsProvider(); + Credentials credentials = new UsernamePasswordCredentials( + MAILCHIMP_USERNAME_PLACEHOLDER, + configuration.getApiKey() + ); + provider.setCredentials(AuthScope.ANY, credentials); + + AuthCache authCache = new BasicAuthCache(); + authCache.put(new HttpHost(configuration.getHost(), configuration.getPort(), configuration.getScheme()), new BasicScheme()); + + HttpClientContext context = HttpClientContext.create(); + + context.setCredentialsProvider(provider); + context.setAuthCache(authCache); + + CloseableHttpClient client = HttpClientBuilder.create() + .build(); + + return new MailchimpInternalHttpClient(configuration, client, context); + } + + private final CloseableHttpClient client; + private final HttpContext context; + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) + .create(); + + private final MailchimpClientConfiguration configuration; + + /** + * This should only be used within the internal client. + */ + @Deprecated + MailchimpInternalHttpClient( + MailchimpClientConfiguration configuration, + CloseableHttpClient client, + HttpContext context) { + this.configuration = configuration; + this.client = client; + this.context = context; + } + + /** + * Completes a get request for a resource, converts the response to + * the given type. + * + * @param uri the location of the resource that the + * request will get from. This is relative to + * the base URL for Mailchimp. + * @param responseClass the class of the response. + * @param the type of the response, taken from the + * given class. + * @return the response object + * @throws MailingListClientException if the resource is unavailable + */ + T get(String uri, Class responseClass) throws MailingListClientException { + HttpGet request = new HttpGet(withBaseUrl(uri)); + + return doRequest(request, responseClass); + } + + /** + * Completes a post request for a resource, converts the given + * request body to JSON and the response to the given type. + * + * @param uri the location of the resource that the + * request will post to. This is relative to + * the base URL for Mailchimp. + * @param body the object for the body, which will be + * converted to JSON. + * @param responseClass the class of the response. + * @param the type of the response, taken from the + * given class. + * @return the response body converted from JSON to the correct + * type. + * @throws MailingListClientException if the resource is unavailable. + */ + T post(String uri, Object body, Class responseClass) throws MailingListClientException { + HttpPost request = new HttpPost(withBaseUrl(uri)); + request.setEntity(createEntity(body)); + + return doRequest(request, responseClass); + } + + /** + * Completes a put request for a resource, converts the given + * request body to JSON and the response to the given type. + * + * @param uri the location of the resource that the + * request will put to. This is relative to + * the base URL for Mailchimp. + * @param body the object for the body, which will be + * converted to JSON. + * @param responseClass the class of the response. + * @param the type of the response, taken from the + * given class. + * @return the response body converted from JSON to the correct + * type. + * @throws MailingListClientException if the resource is unavailable. + */ + T put(String uri, Object body, Class responseClass) throws MailingListClientException { + HttpPut request = new HttpPut(withBaseUrl(uri)); + request.setEntity(createEntity(body)); + + return doRequest(request, responseClass); + } + + /** + * Completes a delete request for a resource, converts the response + * to the given type. + * + * @param uri the location of the resource that the + * request will delete. This is relative to + * the base URL for Mailchimp. + * @param responseClass the class of the response + * @param the type of the response, taken from the + * given class + * @return the response body converted from JSON to the correct + * type. + * @throws MailingListClientException if the resource is unavailable. + */ + T delete(String uri, Class responseClass) throws MailingListClientException { + HttpDelete request = new HttpDelete(withBaseUrl(uri)); + + return doRequest(request, responseClass); + } + + private HttpEntity createEntity(Object object) { + String requestText = gson.toJson(object); + + return new StringEntity(requestText, "UTF-8"); + } + + private String withBaseUrl(String url) { + return configuration.getBaseUrl() + url; + } + + private T doRequest(HttpUriRequest request, Class responseClass) throws MailingListClientException { + LOG.debug("making {} request for {}", request::getMethod, request::getURI); + try { + HttpResponse response = client.execute(request, context); + + manageResponseStatus(request, response); + + LOG.debug("successful {} request for {}", request::getMethod, request::getURI); + + return fromJson(response.getEntity(), responseClass); + } catch (IOException e) { + LOG.error("unable to complete {} request for {}", request.getMethod(), request.getURI(), e); + throw new MailingListClientException("Unable to execute the request " + request.getURI(), e); + } + } + + private void manageResponseStatus( + HttpUriRequest request, + HttpResponse response) throws IOException, MailingListClientException { + + try { + + switch (response.getStatusLine().getStatusCode()) { + case HttpStatus.SC_OK: + case HttpStatus.SC_CREATED: + case HttpStatus.SC_ACCEPTED: + case HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION: + case HttpStatus.SC_NO_CONTENT: + case HttpStatus.SC_RESET_CONTENT: + case HttpStatus.SC_PARTIAL_CONTENT: + case HttpStatus.SC_MULTI_STATUS: + return; + case HttpStatus.SC_NOT_FOUND: + String notFoundErrorMessage = + String.format("Could not find resource for %s. The response body was: \"%s\"", + request.getURI(), + EntityUtils.toString(response.getEntity())); + throw new NotFoundMailingListClientException(notFoundErrorMessage); + default: + String unexpectedErrorMessage = String.format("failed %s request for %s with %s and body \"%s\"", + request.getMethod(), + request.getURI(), + response.getStatusLine(), + EntityUtils.toString(response.getEntity())); + + throw new MailingListClientException(unexpectedErrorMessage); + + } + } catch (MailingListClientException e) { + EntityUtils.consumeQuietly(response.getEntity()); + throw e; + } + } + + private T fromJson(HttpEntity entity, Class responseClass) throws MailingListClientException { + LOG.debug("converting from JSON for {}", responseClass::getCanonicalName); + try { + if (entity != null && entity.getContentLength() != 0) { + String resultText = EntityUtils.toString(entity); + T result = gson.fromJson( + resultText, + responseClass + ); + LOG.debug("converted from JSON for {}, with JSON {}", + responseClass::getCanonicalName, + () -> resultText); + return result; + } else { + return null; + } + } catch (IOException e) { + LOG.error("failed to convert from JSON for {}", responseClass.getCanonicalName()); + throw new MailingListClientException("Unable to read the response", e); + } finally { + EntityUtils.consumeQuietly(entity); + } + } + + +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListDescriptor.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListDescriptor.java new file mode 100644 index 0000000..1bd6bcf --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListDescriptor.java @@ -0,0 +1,39 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import uk.ac.stfc.facilities.mailing.api.data.MailingListDescriptor; + +class MailchimpListDescriptor implements MailingListDescriptor { + private String id; + private String name; + + public MailchimpListDescriptor(String id, String name) { + this.id = id; + this.name = name; + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "MailchimpListDescriptor{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListDescriptors.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListDescriptors.java new file mode 100644 index 0000000..e0f38e6 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListDescriptors.java @@ -0,0 +1,33 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import uk.ac.stfc.facilities.mailing.api.data.MailingListDescriptors; + +import java.util.Set; + +/** + * + */ +class MailchimpListDescriptors implements MailingListDescriptors { + + Set lists; + + public MailchimpListDescriptors(Set lists) { + this.lists = lists; + } + + @Override + public Set getLists() { + return lists; + } + + public void setLists(Set lists) { + this.lists = lists; + } + + @Override + public String toString() { + return "MailchimpListDescriptors{" + + "lists=" + lists + + '}'; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMember.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMember.java new file mode 100644 index 0000000..da6a185 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMember.java @@ -0,0 +1,140 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; +import uk.ac.stfc.facilities.mailing.api.data.MailingListMember; +import uk.ac.stfc.facilities.mailing.api.data.SubscriptionStatus; + +import java.time.LocalDateTime; + +/** + * + */ +class MailchimpListMember implements MailingListMember { + + + private String id; + @SerializedName("email_address") + private String emailAddress; + @SerializedName("unique_email_id") + private String uniqueEmailId; + @SerializedName("email_type") + private String emailType; + @SerializedName("status") + private String status; + + @SerializedName("timestamp_opt") + private LocalDateTime timestampOpt; + @SerializedName("last_change") + private LocalDateTime lastChange; + + @SerializedName("language") + private String language; + @SerializedName("list_id") + private String listId; + + public MailchimpListMember() { + } + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + @Override + public String getUniqueEmailId() { + return uniqueEmailId; + } + + @Override + public SubscriptionStatus getSubscriptionStatus() { + switch (status) { + case "subscribed": + return SubscriptionStatus.SUBSCRIBED; + case "unsubscribed": + return SubscriptionStatus.UNSUBSCRIBED; + default: + return SubscriptionStatus.UNKNOWN; + } + } + + public void setUniqueEmailId(String uniqueEmailId) { + this.uniqueEmailId = uniqueEmailId; + } + + public String getEmailType() { + return emailType; + } + + public void setEmailType(String emailType) { + this.emailType = emailType; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getTimestampOpt() { + return timestampOpt; + } + + public void setTimestampOpt(LocalDateTime timestampOpt) { + this.timestampOpt = timestampOpt; + } + + public LocalDateTime getLastChange() { + return lastChange; + } + + public void setLastChange(LocalDateTime lastChange) { + this.lastChange = lastChange; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + @Override + public String getListId() { + return listId; + } + + public void setListId(String listId) { + this.listId = listId; + } + + @Override + public String toString() { + return "MailchimpListMember{" + + "id='" + id + '\'' + + ", emailAddress='" + emailAddress + '\'' + + ", uniqueEmailId='" + uniqueEmailId + '\'' + + ", emailType='" + emailType + '\'' + + ", status='" + status + '\'' + + ", timestampOpt=" + timestampOpt + + ", lastChange=" + lastChange + + ", language='" + language + '\'' + + ", listId='" + listId + '\'' + + '}'; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMemberRequest.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMemberRequest.java new file mode 100644 index 0000000..72f4d62 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMemberRequest.java @@ -0,0 +1,82 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; + +/** + * + */ +class MailchimpListMemberRequest { + + private static final String SUBSCRIBED = "subscribed"; + private static final String UNSUBSCRIBED = "unsubscribed"; + + public static MailchimpListMemberRequest whichSubscribes(String email) { + MailchimpListMemberRequest request = new MailchimpListMemberRequest(); + + request.email = email; + request.statusIfNew = SUBSCRIBED; + return request; + } + + public static MailchimpListMemberRequest whichForceSubscribes(String email) { + + MailchimpListMemberRequest request = new MailchimpListMemberRequest(); + + request.email = email; + request.statusIfNew = SUBSCRIBED; + request.status = SUBSCRIBED; + return request; + } + + public static MailchimpListMemberRequest whichUnsubscribes(String email) { + + MailchimpListMemberRequest requests = new MailchimpListMemberRequest(); + + requests.email = email; + requests.statusIfNew = UNSUBSCRIBED; + return requests; + } + + public static MailchimpListMemberRequest whichForceUnsubscribes(String email) { + + MailchimpListMemberRequest request = new MailchimpListMemberRequest(); + + request.email = email; + request.statusIfNew = UNSUBSCRIBED; + request.status = UNSUBSCRIBED; + return request; + } + + @SerializedName("email_address") + private String email; + + @SerializedName("status_if_new") + private String statusIfNew; + + @SerializedName("status") + private String status; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getStatusIfNew() { + return statusIfNew; + } + + public void setStatusIfNew(String statusIfNew) { + this.statusIfNew = statusIfNew; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMembers.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMembers.java new file mode 100644 index 0000000..e163cec --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMembers.java @@ -0,0 +1,33 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import uk.ac.stfc.facilities.mailing.api.data.MailingListMembers; + +import java.util.Set; + +/** + * + */ +class MailchimpListMembers implements MailingListMembers { + + Set members; + + public MailchimpListMembers(Set members) { + this.members = members; + } + + @Override + public Set getMembers() { + return members; + } + + public void setMembers(Set members) { + this.members = members; + } + + @Override + public String toString() { + return "MailchimpListMembers{" + + "members=" + members + + '}'; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentCreateRequest.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentCreateRequest.java new file mode 100644 index 0000000..ffabc00 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentCreateRequest.java @@ -0,0 +1,42 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashSet; + +/** + * + */ +public class MailchimpSegmentCreateRequest { + + + @SerializedName("name") + private String name; + + @SerializedName("static_segment") + private HashSet emails; + + public static MailchimpSegmentCreateRequest whichCreatesASegment(String segmentName) { + MailchimpSegmentCreateRequest createRequest = new MailchimpSegmentCreateRequest(); + createRequest.name = segmentName; + createRequest.emails = new HashSet<>(); + + return createRequest; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public HashSet getEmails() { + return emails; + } + + public void setEmails(HashSet emails) { + this.emails = emails; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentDescriptor.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentDescriptor.java new file mode 100644 index 0000000..c250498 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentDescriptor.java @@ -0,0 +1,37 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import uk.ac.stfc.facilities.mailing.api.data.MailingListSegmentDescriptor; + +/** + * + */ +class MailchimpSegmentDescriptor implements MailingListSegmentDescriptor { + private String id; + private String name; + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "MailchimpSegmentDescriptor{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentDescriptors.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentDescriptors.java new file mode 100644 index 0000000..3675676 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentDescriptors.java @@ -0,0 +1,36 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; +import uk.ac.stfc.facilities.mailing.api.data.MailingListSegmentDescriptors; + +import java.util.Iterator; +import java.util.Set; + +/** + * + */ +class MailchimpSegmentDescriptors implements MailingListSegmentDescriptors { + + @SerializedName("segments") + Set segmentDescriptors; + + @Override + public Set getSegmentDescriptors() { + return segmentDescriptors; + } + + public Set setSegmentDescriptors() { + return segmentDescriptors; + } + + public Iterator iterator() { + return segmentDescriptors.iterator(); + } + + @Override + public String toString() { + return "MailchimpSegmentDescriptors{" + + "segmentDescriptors=" + segmentDescriptors + + '}'; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberChanges.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberChanges.java new file mode 100644 index 0000000..8dbdedc --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberChanges.java @@ -0,0 +1,43 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; +import uk.ac.stfc.facilities.mailing.api.data.MailingListSegmentMemberChanges; + +import java.util.Set; + +/** + * + */ +class MailchimpSegmentMemberChanges implements MailingListSegmentMemberChanges { + @SerializedName("members_added") + private Set addedMembers; + @SerializedName("members_removed") + private Set removedMembers; + + @Override + public Set getAddedMembers() { + return addedMembers; + } + + @Override + public Set getRemovedMembers() { + return removedMembers; + } + + public void setAddedMembers(Set addedMembers) { + this.addedMembers = addedMembers; + } + + public void setRemovedMembers(Set removedMembers) { + this.removedMembers = removedMembers; + } + + @Override + public String toString() { + return "MailchimpSegmentMemberChanges{" + + "addedMembers=" + addedMembers + + ", removedMembers=" + removedMembers + + '}'; + } + +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberRequest.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberRequest.java new file mode 100644 index 0000000..0807454 --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberRequest.java @@ -0,0 +1,44 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; + +import java.util.Set; + +/** + * + */ +class MailchimpSegmentMemberRequest { + + @SerializedName("members_to_add") + private Set membersToAdd; + @SerializedName("members_to_remove") + private Set membersToRemove; + + public static MailchimpSegmentMemberRequest whichAddsMembers(Set email) { + MailchimpSegmentMemberRequest request = new MailchimpSegmentMemberRequest(); + request.membersToAdd = email; + return request; + } + + public static MailchimpSegmentMemberRequest whichRemovesMembers(Set email) { + MailchimpSegmentMemberRequest request = new MailchimpSegmentMemberRequest(); + request.membersToRemove = email; + return request; + } + + public Set getMembersToAdd() { + return membersToAdd; + } + + public void setMembersToAdd(Set membersToAdd) { + this.membersToAdd = membersToAdd; + } + + public Set getMembersToRemove() { + return membersToRemove; + } + + public void setMembersToRemove(Set membersToRemove) { + this.membersToRemove = membersToRemove; + } +} diff --git a/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/gson/adapters/LocalDateTimeAdapter.java b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/gson/adapters/LocalDateTimeAdapter.java new file mode 100644 index 0000000..50af65e --- /dev/null +++ b/mailing-list-client/src/main/java/uk/ac/stfc/facilities/mailing/mailchimp/gson/adapters/LocalDateTimeAdapter.java @@ -0,0 +1,22 @@ +package uk.ac.stfc.facilities.mailing.mailchimp.gson.adapters; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * + */ +public class LocalDateTimeAdapter implements JsonDeserializer { + + + @Override + public LocalDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return LocalDateTime.parse(jsonElement.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ISO_DATE_TIME); + } +} diff --git a/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClientIT.java b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClientIT.java new file mode 100644 index 0000000..ec430a5 --- /dev/null +++ b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpClientIT.java @@ -0,0 +1,431 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import org.junit.jupiter.api.*; +import uk.ac.stfc.facilities.mailing.api.data.*; +import uk.ac.stfc.facilities.mailing.api.exceptions.MailingListClientException; +import uk.ac.stfc.facilities.mailing.api.exceptions.NotFoundMailingListClientException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Properties; +import java.util.Random; + +import static org.assertj.core.api.Assertions.*; + +@DisplayName("The Mailchimp client") +public class MailchimpClientIT { + + private static final String NAME_FIELD_NAME = "name"; + private static final String ID_FIELD_NAME = "id"; + private static final String EMAIL_ADDRESS_FIELD_NAME = "emailAddress"; + private static final String SUBSCRIPTION_STATUS_FIELD_NAME = "subscriptionStatus"; + + private static final String EXPECTED_SEGMENT_NAME = "test"; + + private static final SubscriptionStatus PERMANENT_SUBSCRIPTION_STATUS_1 = SubscriptionStatus.SUBSCRIBED; + private static final SubscriptionStatus PERMANENT_SUBSCRIPTION_STATUS_2 = SubscriptionStatus.SUBSCRIBED; + private static final SubscriptionStatus PERMANENT_SUBSCRIPTION_STATUS_3 = SubscriptionStatus.UNSUBSCRIBED; + + private static String expectedUserEmail; + + private static String listId; + private static String listName; + + private static String testDomain; + + private static String noSuchMemberEmail; + + private static String permanentEmail1; + private static String permanentEmail2; + private static String permanentEmail3; + + private static MailchimpClient mailchimpClient; + + private static String loadProperty(Properties properties, String key) { + final String EXCEPTION_MESSAGE = "" + + "\"{}\" was not specified in the application.test.properties file. This key is required to run these" + + " tests. This file should be placed in the directory that the tests are running from."; + + String value = properties.getProperty(key); + + if (value == null || value.isEmpty()) { + throw new RuntimeException(EXCEPTION_MESSAGE.replace("{}", key)); + } + + return value; + } + + @BeforeAll + public static void setupClass() throws IOException { + Properties properties = new Properties(); + + File file = new File("application.test.properties"); + + properties.load(new FileInputStream(file)); + + String mailchimpApiKey = loadProperty(properties, "mailchimp.api.key"); + + mailchimpClient = MailchimpClient.getInstance(new MailchimpClientConfiguration(mailchimpApiKey)); + + listId = loadProperty(properties, "mailchimp.list.id"); + + listName = loadProperty(properties, "mailchimp.list.name"); + + testDomain = loadProperty(properties, "mailchimp.test.domain"); + + permanentEmail1 = "permanent.test.1@" + testDomain; + permanentEmail2 = "permanent.test.2@" + testDomain; + permanentEmail3 = "permanent.test.3@" + testDomain; + + noSuchMemberEmail = "no.such.member@" + testDomain; + + } + + @BeforeEach + public void generateRandomUser() throws IOException { + Random random = new Random(); + + expectedUserEmail = random.ints(20) + .map(i -> Math.abs(i % 26) + 97) + .mapToObj(i -> Character.toString((char) i)) + .reduce("", (s, i) -> s + i) + "@test.stfc.co.uk"; + + } + + @Nested + @DisplayName("for lists") + public class Lists { + + @Test + @DisplayName("is able to get all lists") + public void getAllListDescriptors() throws MailingListClientException { + + MailingListDescriptors descriptors = mailchimpClient.getAllListDescriptors(); + + assertThat(descriptors.getLists()) + .extracting(ID_FIELD_NAME, NAME_FIELD_NAME) + .contains(tuple(listId, listName)); + } + + @Test + @DisplayName("is able to get a list by ID") + public void getListDescriptor() throws MailingListClientException { + MailingListDescriptor descriptor = mailchimpClient.getListDescriptor(listId); + assertThat(descriptor.getId()).isEqualTo(listId); + assertThat(descriptor.getName()).isEqualTo(listName); + + } + + @Nested + @DisplayName("for members") + public class Members { + + @Nested + @DisplayName("when retrieving") + public class Retriving { + @BeforeEach + public void ensureRandomUserExists() throws MailingListClientException { + + addExpectedUser(); + } + + @AfterEach + public void ensureRandomUserIsRemoved() throws MailingListClientException { + removeExpectedUser(); + } + + @Test + @DisplayName("is able to get all members of a list by list ID") + public void getMembersByList() throws MailingListClientException { + MailingListMembers mailingListMembers = mailchimpClient.getMembersByList(listId); + assertThat(mailingListMembers.getMembers()) + .extracting(EMAIL_ADDRESS_FIELD_NAME, SUBSCRIPTION_STATUS_FIELD_NAME) + .contains( + tuple(permanentEmail1, PERMANENT_SUBSCRIPTION_STATUS_1), + tuple(permanentEmail2, PERMANENT_SUBSCRIPTION_STATUS_2), + tuple(permanentEmail3, PERMANENT_SUBSCRIPTION_STATUS_3) + ); + + } + + @Test + @DisplayName("is able to get a member of a list by list ID and member email") + public void getMemberOfList() throws MailingListClientException { + MailingListMember member = mailchimpClient.getMemberOfList(listId, expectedUserEmail); + + assertThat(member.getEmailAddress()).isEqualTo(expectedUserEmail); + } + + @Test + @DisplayName("throws a NotFoundMailingListClientException when a member cannot be found in a list") + public void getMemberOfList_notFound() throws MailingListClientException { + assertThatThrownBy(() -> mailchimpClient.getMemberOfList(listId, noSuchMemberEmail)) + .isInstanceOf(NotFoundMailingListClientException.class); + } + + } + + @Nested + @DisplayName("when deleting") + public class Deleting { + + @Nested + @DisplayName("can delete") + public class Successfully { + + @BeforeEach + public void ensureRandomUserExists() throws MailingListClientException { + + addExpectedUser(); + } + + @Test + @DisplayName("members from a list") + public void deleteMemberFromList() throws MailingListClientException { + mailchimpClient.deleteMemberFromList(listId, expectedUserEmail); + } + } + + @Nested + @DisplayName("a non-existant member") + public class NonExistantMember { + + @Test + @DisplayName("can't be deleted") + public void deleteMemberFromList() throws MailingListClientException { + assertThatThrownBy(() -> mailchimpClient.deleteMemberFromList(listId, expectedUserEmail)) + .isInstanceOf(NotFoundMailingListClientException.class); + } + } + } + + + + } + } + + @Nested + @DisplayName("when changing the subscription status of") + public class Subscribing { + + @Nested + @DisplayName("a new user") + public class New { + + @Test + @DisplayName("to be subscribed their status should be subscribed") + public void subscribeUser() throws MailingListClientException { + MailingListMember member = mailchimpClient.subscribeMember(listId, expectedUserEmail); + assertThat(member.getSubscriptionStatus()).isEqualTo(SubscriptionStatus.SUBSCRIBED); + + removeExpectedUser(); + } + + @Test + @DisplayName("to be unsubscribed their status should be unsubscribed") + public void unsubscribeUser() throws MailingListClientException { + MailingListMember member = mailchimpClient.unsubscribeMember(listId, expectedUserEmail); + assertThat(member.getSubscriptionStatus()).isEqualTo(SubscriptionStatus.UNSUBSCRIBED); + + removeExpectedUser(); + } + + } + + @Nested + @DisplayName("an existing user") + public class Existing { + + @BeforeEach + public void ensureUserExists() throws MailingListClientException { + addExpectedUser(); + } + + @AfterEach + public void ensureUserRemoved() throws MailingListClientException { + removeExpectedUser(); + } + + @Test + @DisplayName("to be force subscribed their status should be subscribed") + public void forceSubscribeUser() throws MailingListClientException { + MailingListMember member = mailchimpClient.forceSubscribeMember(listId, expectedUserEmail); + assertThat(member.getSubscriptionStatus()).isEqualTo(SubscriptionStatus.SUBSCRIBED); + } + + @Test + @DisplayName("to be force unsubscribed their status should be unsubscribed") + public void forceUnsubscribeUser() throws MailingListClientException { + MailingListMember member = mailchimpClient.forceUnsubscribeMember(listId, expectedUserEmail); + assertThat(member.getSubscriptionStatus()).isEqualTo(SubscriptionStatus.UNSUBSCRIBED); + } + } + + } + + @Nested + @DisplayName("for segment") + public class Segments { + + + @Nested + @DisplayName("creation") + public class New { + + private String segmentId; + + @AfterEach + public void ensureSegmentDeleted() throws MailingListClientException { + mailchimpClient.deleteSegment(listId, segmentId); + } + + @Test + @DisplayName("the segment is created successfully") + public void createSegment() throws MailingListClientException { + MailingListSegmentDescriptor descriptor = mailchimpClient.createSegment( + listId, EXPECTED_SEGMENT_NAME); + + segmentId = descriptor.getId(); + } + } + + @Nested + @DisplayName("retrieval") + public class Existing { + + private String segmentId; + + @BeforeEach + public void ensureSegmentExists() throws MailingListClientException { + segmentId = mailchimpClient.createSegment(listId, EXPECTED_SEGMENT_NAME).getId(); + + } + + @AfterEach + public void ensureSegmentDeleted() throws MailingListClientException { + mailchimpClient.deleteSegment(listId, segmentId); + } + + @Test + @DisplayName("all of the segment descriptors are returned correctly") + public void getSegmentDescriptors() throws MailingListClientException { + MailingListSegmentDescriptors descriptors = mailchimpClient.getSegmentDescriptors(listId); + + assertThat(descriptors.getSegmentDescriptors()) + .extracting(NAME_FIELD_NAME, ID_FIELD_NAME) + .contains(tuple(EXPECTED_SEGMENT_NAME, segmentId)); + } + + @Test + @DisplayName("the correct segment, by given ID, is returned correctly") + public void getSegmentDescriptor() throws MailingListClientException { + mailchimpClient.getSegmentDescriptor(listId, segmentId); + MailingListSegmentDescriptor descriptor = mailchimpClient.getSegmentDescriptor(listId, segmentId); + + assertThat(descriptor.getId()).isEqualTo(segmentId); + assertThat(descriptor.getName()).isEqualTo(EXPECTED_SEGMENT_NAME); + } + } + + @Nested + @DisplayName("members") + public class Members { + + MailingListSegmentDescriptor descriptor; + + @BeforeEach + public void ensureUserExists() throws MailingListClientException { + addExpectedUser(); + + descriptor = mailchimpClient.createSegment(listId, EXPECTED_SEGMENT_NAME); + + } + + @AfterEach + public void ensureUserRemoved() throws MailingListClientException { + removeExpectedUser(); + + mailchimpClient.deleteSegment(listId, descriptor.getId()); + } + + @Nested + @DisplayName("which are new") + public class New { + + @Test + @DisplayName("the members are correctly added to the segment") + public void addMembersToSegment() throws MailingListClientException { + + MailingListSegmentMemberChanges changes = mailchimpClient.addMembersToSegment( + listId, + descriptor.getId(), + new HashSet<>(Arrays.asList(expectedUserEmail)) + ); + + assertThat(changes.getAddedMembers()) + .extracting(EMAIL_ADDRESS_FIELD_NAME) + .contains(expectedUserEmail); + + } + } + + @Nested + @DisplayName("which are existing") + public class Existing { + + @BeforeEach + public void ensureMemberIsAddedToSegment() throws MailingListClientException { + mailchimpClient.addMembersToSegment( + listId, + descriptor.getId(), + new HashSet<>(Arrays.asList(expectedUserEmail)) + ); + } + + @Test + @DisplayName("they can be retrieved") + public void getMembersOfSegment() throws MailingListClientException { + + MailingListMembers members = mailchimpClient.getMembersOfSegment( + listId, + descriptor.getId() + ); + + assertThat(members.getMembers()) + .extracting(EMAIL_ADDRESS_FIELD_NAME) + .contains(expectedUserEmail); + } + + @Test + @DisplayName("they can be removed") + public void removeMembersFromSegment() throws MailingListClientException { + + MailingListSegmentMemberChanges changes = mailchimpClient.removeMembersFromSegment( + listId, + descriptor.getId(), + new HashSet<>(Arrays.asList(expectedUserEmail)) + ); + + assertThat(changes.getRemovedMembers()) + .extracting(EMAIL_ADDRESS_FIELD_NAME) + .contains(expectedUserEmail); + + } + } + } + + } + + private static void addExpectedUser() throws MailingListClientException { + mailchimpClient.subscribeMember(listId, expectedUserEmail); + } + + private static void removeExpectedUser() throws MailingListClientException { + mailchimpClient.deleteMemberFromList(listId, expectedUserEmail); + } + + +} diff --git a/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpInternalHttpClientTest.java b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpInternalHttpClientTest.java new file mode 100644 index 0000000..e50db1b --- /dev/null +++ b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpInternalHttpClientTest.java @@ -0,0 +1,286 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + +import com.google.gson.annotations.SerializedName; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.*; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import uk.ac.stfc.facilities.mailing.api.exceptions.MailingListClientException; +import uk.ac.stfc.facilities.mailing.api.exceptions.NotFoundMailingListClientException; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.*; + +/** + * + */ +public class MailchimpInternalHttpClientTest { + + public static class TestDto { + + @SerializedName("value") + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public TestDto(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TestDto testDto = (TestDto) o; + + return value != null ? value.equals(testDto.value) : testDto.value == null; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + } + + // dash is required in the api key + private static final MailchimpClientConfiguration EXAMPLE_CONFIGURATION = + new MailchimpClientConfiguration("abcd-ab1"); + + private static final IOException EXPECTED_IO_EXCEPTION_CAUSE = new IOException("test"); + private static final String TEST_DTO_JSON = "{\"value\":\"hello world\"}"; + private static final TestDto EXPECTED_TEST_DTO = new TestDto("hello world"); + + public static Collection methodProvider() { + + Function getFunction = + httpClient -> () -> httpClient.get("", TestDto.class); + + Function putFunction = + httpClient -> () -> httpClient.put("", EXPECTED_TEST_DTO, TestDto.class); + + Function postFunction = + httpClient -> () -> httpClient.post("", EXPECTED_TEST_DTO, TestDto.class); + + Function deleteFunction = + httpClient -> () -> httpClient.delete("", TestDto.class); + + return Arrays.asList( + Arguments.of(getFunction, HttpGet.class, false), + Arguments.of(putFunction, HttpPut.class, true), + Arguments.of(postFunction, HttpPost.class, true), + Arguments.of(deleteFunction, HttpDelete.class, false) + ); + } + + + private CloseableHttpClient mockedHttpClient; + private HttpContext mockedHttpContext; + private CloseableHttpResponse mockedHttpResponse; + private HttpEntity mockedHttpEntity; + + @BeforeEach + public void setupHttpMock() { + this.mockedHttpClient = mock(CloseableHttpClient.class); + this.mockedHttpContext = mock(HttpContext.class); + this.mockedHttpResponse = mock(CloseableHttpResponse.class, Mockito.RETURNS_DEEP_STUBS); + this.mockedHttpEntity = mock(HttpEntity.class); + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_successfulResponse_entitySet( + Function> method, + Class methodType, + boolean expectRequestBody + ) throws Exception { + + MailchimpInternalHttpClient client = setupResponseWithBody(methodType); + + assertThat(method.apply(client).call()).isEqualTo(EXPECTED_TEST_DTO); + + if (expectRequestBody) { + verifyExpectedRequestBody(); + } + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_successfulResponse_correctResult( + Function> method, + Class methodType + ) throws Exception { + + MailchimpInternalHttpClient client = setupResponseWithBody(methodType); + + assertThat(method.apply(client).call()).isEqualTo(EXPECTED_TEST_DTO); + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_unexpectedIOException_errorThrown( + Function> method, + Class methodType + ) throws Exception { + + MailchimpInternalHttpClient httpClient = setupIOExceptionOnRequestExecution(methodType); + + assertThatThrownBy(method.apply(httpClient)::call) + .isInstanceOf(MailingListClientException.class) + .hasCause(EXPECTED_IO_EXCEPTION_CAUSE); + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_404_errorThrown( + Function> method, + Class methodType + ) throws Exception { + + MailchimpInternalHttpClient httpClient = setupResponseWithStatusCode(methodType, HttpStatus.SC_NOT_FOUND); + + assertThatThrownBy(method.apply(httpClient)::call) + .isInstanceOf(NotFoundMailingListClientException.class) + .hasNoCause(); + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_unexpectedStatusCode_errorThrown( + Function> method, + Class methodType + ) throws Exception { + + MailchimpInternalHttpClient httpClient = setupResponseWithStatusCode(methodType, HttpStatus.SC_INTERNAL_SERVER_ERROR); + + assertThatThrownBy(method.apply(httpClient)::call) + .isInstanceOf(MailingListClientException.class) + .hasNoCause(); + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_entityIOException_errorThrown( + Function> method, + Class methodType + ) throws Exception { + + MailchimpInternalHttpClient httpClient = setupExceptionWhileRetrievingContent(methodType); + + assertThatThrownBy(method.apply(httpClient)::call) + .isInstanceOf(MailingListClientException.class) + .hasCause(EXPECTED_IO_EXCEPTION_CAUSE); + } + + @ParameterizedTest + @MethodSource("methodProvider") + public void onRequest_nullEntity_nothingReturned( + Function> method, + Class methodType + ) throws Exception { + + MailchimpInternalHttpClient httpClient = setupResponseWithNullEntity(methodType); + + assertThat(method.apply(httpClient).call()) + .isNull(); + } + + + private void verifyExpectedRequestBody() throws IOException { + ArgumentCaptor argumentCaptor + = ArgumentCaptor.forClass(HttpEntityEnclosingRequestBase.class); + verify(mockedHttpClient).execute(argumentCaptor.capture(), eq(mockedHttpContext)); + HttpEntityEnclosingRequestBase capturedArgument = argumentCaptor.getValue(); + + String entityContent = EntityUtils.toString(capturedArgument.getEntity()) + .replaceAll("\\s", ""); + + String expectedContent = TEST_DTO_JSON.replaceAll("\\s", ""); + + assertThat(entityContent).isEqualTo(expectedContent); + } + + private MailchimpInternalHttpClient setupExceptionWhileRetrievingContent(Class requestClass) throws IOException { + + when(mockedHttpResponse.getStatusLine().getStatusCode()).thenReturn(HttpStatus.SC_OK); + + when(mockedHttpResponse.getEntity()).thenReturn(mockedHttpEntity); + + when(mockedHttpEntity.getContentLength()).thenReturn(100L); + when(mockedHttpEntity.getContent()).thenThrow(EXPECTED_IO_EXCEPTION_CAUSE); + + when(mockedHttpClient.execute(isA(requestClass), eq(mockedHttpContext))) + .thenReturn(mockedHttpResponse); + + + return new MailchimpInternalHttpClient(EXAMPLE_CONFIGURATION, mockedHttpClient, mockedHttpContext); + } + + private MailchimpInternalHttpClient setupResponseWithBody(Class requestClass) throws IOException { + + when(mockedHttpResponse.getStatusLine().getStatusCode()).thenReturn(HttpStatus.SC_OK); + + when(mockedHttpResponse.getEntity()).thenReturn(new StringEntity(TEST_DTO_JSON)); + + when(mockedHttpClient.execute(isA(requestClass), eq(mockedHttpContext))) + .thenReturn(mockedHttpResponse); + + + return new MailchimpInternalHttpClient(EXAMPLE_CONFIGURATION, mockedHttpClient, mockedHttpContext); + } + + private MailchimpInternalHttpClient setupIOExceptionOnRequestExecution(Class requestClass) throws IOException { + + when(mockedHttpClient.execute(isA(requestClass), eq(mockedHttpContext))) + .thenThrow(EXPECTED_IO_EXCEPTION_CAUSE); + + return new MailchimpInternalHttpClient(EXAMPLE_CONFIGURATION, mockedHttpClient, mockedHttpContext); + } + + private MailchimpInternalHttpClient setupResponseWithStatusCode(Class requestClass, int statusCode) throws IOException { + + when(mockedHttpResponse.getStatusLine().getStatusCode()).thenReturn(statusCode); + + when(mockedHttpResponse.getEntity()).thenReturn(new StringEntity("hello world")); + + when(mockedHttpClient.execute(isA(requestClass), eq(mockedHttpContext))) + .thenReturn(mockedHttpResponse); + + return new MailchimpInternalHttpClient(EXAMPLE_CONFIGURATION, mockedHttpClient, mockedHttpContext); + } + + private MailchimpInternalHttpClient setupResponseWithNullEntity(Class requestClass) throws IOException { + + when(mockedHttpResponse.getStatusLine().getStatusCode()).thenReturn(HttpStatus.SC_OK); + when(mockedHttpResponse.getEntity()).thenReturn(null); + + when(mockedHttpClient.execute(isA(requestClass), eq(mockedHttpContext))) + .thenReturn(mockedHttpResponse); + + return new MailchimpInternalHttpClient(EXAMPLE_CONFIGURATION, mockedHttpClient, mockedHttpContext); + } +} diff --git a/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMemberRequestTest.java b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMemberRequestTest.java new file mode 100644 index 0000000..01113a6 --- /dev/null +++ b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpListMemberRequestTest.java @@ -0,0 +1,52 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + */ +public class MailchimpListMemberRequestTest { + + @Test + public void whichSubscribes() { + MailchimpListMemberRequest request = MailchimpListMemberRequest.whichSubscribes("test"); + + assertThat(request.getEmail()).isEqualTo("test"); + assertThat(request.getStatusIfNew()).isEqualTo("subscribed"); + assertThat(request.getStatus()).isNull(); + + } + + @Test + public void whichUnsubscribes() { + MailchimpListMemberRequest request = MailchimpListMemberRequest.whichUnsubscribes("test"); + + assertThat(request.getEmail()).isEqualTo("test"); + assertThat(request.getStatusIfNew()).isEqualTo("unsubscribed"); + assertThat(request.getStatus()).isNull(); + + } + + @Test + public void whichForceSubscribes() { + MailchimpListMemberRequest request = MailchimpListMemberRequest.whichForceSubscribes("test"); + + assertThat(request.getEmail()).isEqualTo("test"); + assertThat(request.getStatusIfNew()).isEqualTo("subscribed"); + assertThat(request.getStatus()).isEqualTo("subscribed"); + + } + + @Test + public void whichForceUnsubscribes() { + MailchimpListMemberRequest request = MailchimpListMemberRequest.whichForceUnsubscribes("test"); + + assertThat(request.getEmail()).isEqualTo("test"); + assertThat(request.getStatusIfNew()).isEqualTo("unsubscribed"); + assertThat(request.getStatus()).isEqualTo("unsubscribed"); + + } +} diff --git a/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentCreateRequestTest.java b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentCreateRequestTest.java new file mode 100644 index 0000000..8c73cee --- /dev/null +++ b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentCreateRequestTest.java @@ -0,0 +1,20 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + */ +public class MailchimpSegmentCreateRequestTest { + + @Test + public void whichCreatesASegment() { + MailchimpSegmentCreateRequest request = MailchimpSegmentCreateRequest.whichCreatesASegment("test"); + + assertThat(request.getName()).isEqualTo("test"); + } + +} diff --git a/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberRequestTest.java b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberRequestTest.java new file mode 100644 index 0000000..2eb16b9 --- /dev/null +++ b/mailing-list-client/src/test/java/uk/ac/stfc/facilities/mailing/mailchimp/MailchimpSegmentMemberRequestTest.java @@ -0,0 +1,37 @@ +package uk.ac.stfc.facilities.mailing.mailchimp; + + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashSet; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + */ +public class MailchimpSegmentMemberRequestTest { + + + @Test + public void whichAddsMembers() { + MailchimpSegmentMemberRequest request = MailchimpSegmentMemberRequest.whichAddsMembers( + new HashSet<>(Arrays.asList("test", "email")) + ); + + assertThat(request.getMembersToAdd()).contains("test", "email"); + assertThat(request.getMembersToRemove()).isNullOrEmpty(); + } + + @Test + public void whichRemovesMembers() { + MailchimpSegmentMemberRequest request = MailchimpSegmentMemberRequest.whichRemovesMembers( + new HashSet<>(Arrays.asList("test", "email")) + ); + + assertThat(request.getMembersToRemove()).contains("test", "email"); + assertThat(request.getMembersToAdd()).isNullOrEmpty(); + } + +}