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 extends MailingListDescriptor> 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 extends MailingListMember> 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 extends MailingListSegmentDescriptor> 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 extends MailingListMember> getAddedMembers();
+
+ /**
+ * @return the set of members removed from the segment
+ */
+ Set extends MailingListMember> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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 extends HttpUriRequest> 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();
+ }
+
+}