From 97ab735811f36aa0836e3f4acf44d99bffd59141 Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Thu, 23 Jul 2026 10:07:52 -0300 Subject: [PATCH 1/2] feat: Add suppression module --- .../services/suppressions/Suppressions.java | 133 ++++++++ .../suppressions/SuppressionsBatch.java | 69 ++++ .../model/AddSuppressionOptions.java | 72 ++++ .../model/AddSuppressionResponseSuccess.java | 51 +++ .../model/AddSuppressionsOptions.java | 90 +++++ .../model/AddSuppressionsResponseSuccess.java | 39 +++ .../suppressions/model/AddedSuppression.java | 51 +++ .../model/GetSuppressionResponseSuccess.java | 108 ++++++ .../model/ListSuppressionsParams.java | 172 ++++++++++ .../ListSuppressionsResponseSuccess.java | 67 ++++ .../RemoveSuppressionResponseSuccess.java | 65 ++++ .../model/RemoveSuppressionsOptions.java | 131 ++++++++ .../RemoveSuppressionsResponseSuccess.java | 39 +++ .../model/RemovedSuppression.java | 65 ++++ .../suppressions/model/Suppression.java | 108 ++++++ .../suppressions/model/SuppressionOrigin.java | 49 +++ .../suppressions/SuppressionsTest.java | 315 ++++++++++++++++++ .../services/util/SuppressionsUtil.java | 92 +++++ 18 files changed, 1716 insertions(+) create mode 100644 src/main/java/com/resend/services/suppressions/Suppressions.java create mode 100644 src/main/java/com/resend/services/suppressions/SuppressionsBatch.java create mode 100644 src/main/java/com/resend/services/suppressions/model/AddSuppressionOptions.java create mode 100644 src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java create mode 100644 src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java create mode 100644 src/main/java/com/resend/services/suppressions/model/AddSuppressionsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/suppressions/model/AddedSuppression.java create mode 100644 src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java create mode 100644 src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java create mode 100644 src/main/java/com/resend/services/suppressions/model/ListSuppressionsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java create mode 100644 src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java create mode 100644 src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsResponseSuccess.java create mode 100644 src/main/java/com/resend/services/suppressions/model/RemovedSuppression.java create mode 100644 src/main/java/com/resend/services/suppressions/model/Suppression.java create mode 100644 src/main/java/com/resend/services/suppressions/model/SuppressionOrigin.java create mode 100644 src/test/java/com/resend/services/suppressions/SuppressionsTest.java create mode 100644 src/test/java/com/resend/services/util/SuppressionsUtil.java diff --git a/src/main/java/com/resend/services/suppressions/Suppressions.java b/src/main/java/com/resend/services/suppressions/Suppressions.java new file mode 100644 index 0000000..8c541f1 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/Suppressions.java @@ -0,0 +1,133 @@ +package com.resend.services.suppressions; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.service.BaseService; +import com.resend.services.suppressions.model.*; +import okhttp3.MediaType; + +/** + * Represents the Resend Suppressions module. + */ +public class Suppressions extends BaseService { + + /** + * Constructs an instance of the {@code Suppressions} class. + * + * @param apiKey The apiKey used for authentication. + */ + public Suppressions(final String apiKey) { + super(apiKey); + } + + Suppressions(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Returns a SuppressionsBatch object that can be used to add or remove suppressions in batch. + * + * @return A SuppressionsBatch object. + */ + public SuppressionsBatch batch() { + return new SuppressionsBatch(super.apiKey, super.httpClient); + } + + /** + * Adds an email address to the suppression list. + * + * @param addSuppressionOptions The suppression details. + * @return The details of the added suppression. + * @throws ResendException If an error occurs during the suppression creation process. + */ + public AddSuppressionResponseSuccess add(AddSuppressionOptions addSuppressionOptions) throws ResendException { + String payload = super.resendMapper.writeValue(addSuppressionOptions); + AbstractHttpResponse response = httpClient.perform("/suppressions", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, AddSuppressionResponseSuccess.class); + } + + /** + * Removes a suppression by its unique identifier or by the suppressed email address. + * + * @param suppressionIdOrEmail The unique identifier or the suppressed email address. + * @return The RemoveSuppressionResponseSuccess with the details of the removed suppression. + * @throws ResendException If an error occurs during the suppression removal process. + */ + public RemoveSuppressionResponseSuccess remove(String suppressionIdOrEmail) throws ResendException { + AbstractHttpResponse response = httpClient.perform("/suppressions/" + suppressionIdOrEmail, super.apiKey, HttpMethod.DELETE, null, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, RemoveSuppressionResponseSuccess.class); + } + + /** + * Retrieves a suppression by its unique identifier or by the suppressed email address. + * + * @param suppressionIdOrEmail The unique identifier or the suppressed email address. + * @return The retrieved suppression details. + * @throws ResendException If an error occurs while retrieving the suppression. + */ + public GetSuppressionResponseSuccess get(String suppressionIdOrEmail) throws ResendException { + AbstractHttpResponse response = this.httpClient.perform("/suppressions/" + suppressionIdOrEmail, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, GetSuppressionResponseSuccess.class); + } + + /** + * Retrieves a list of suppressions. + * + * @return A ListSuppressionsResponseSuccess containing the list of suppressions. + * @throws ResendException If an error occurs during the suppressions list retrieval process. + */ + public ListSuppressionsResponseSuccess list() throws ResendException { + AbstractHttpResponse response = this.httpClient.perform("/suppressions", super.apiKey, HttpMethod.GET, null, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, ListSuppressionsResponseSuccess.class); + } + + /** + * Retrieves a paginated list of suppressions with optional filtering by origin. + * + * @param params The params used to customize the list. + * @return A ListSuppressionsResponseSuccess containing the paginated list of suppressions. + * @throws ResendException If an error occurs during the suppressions list retrieval process. + */ + public ListSuppressionsResponseSuccess list(ListSuppressionsParams params) throws ResendException { + String pathWithQuery = "/suppressions" + (params != null ? params.toQueryString() : ""); + AbstractHttpResponse response = this.httpClient.perform(pathWithQuery, super.apiKey, HttpMethod.GET, null, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, ListSuppressionsResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/suppressions/SuppressionsBatch.java b/src/main/java/com/resend/services/suppressions/SuppressionsBatch.java new file mode 100644 index 0000000..8f26c30 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/SuppressionsBatch.java @@ -0,0 +1,69 @@ +package com.resend.services.suppressions; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.core.service.BaseService; +import com.resend.services.suppressions.model.*; +import okhttp3.MediaType; + +/** + * Represents the Resend Suppressions Batch module. + */ +public class SuppressionsBatch extends BaseService { + + /** + * Constructs an instance of the {@code SuppressionsBatch} class. + * + * @param apiKey The apiKey used for authentication. + */ + public SuppressionsBatch(final String apiKey) { + super(apiKey); + } + + SuppressionsBatch(final String apiKey, final IHttpClient httpClient) { + super(apiKey, httpClient); + } + + /** + * Adds up to 100 email addresses to the suppression list at once. + * + * @param addSuppressionsOptions The email addresses to suppress. + * @return The AddSuppressionsResponseSuccess with the details of the added suppressions. + * @throws ResendException If an error occurs during the suppressions creation process. + */ + public AddSuppressionsResponseSuccess add(AddSuppressionsOptions addSuppressionsOptions) throws ResendException { + String payload = super.resendMapper.writeValue(addSuppressionsOptions); + AbstractHttpResponse response = httpClient.perform("/suppressions/batch/add", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, AddSuppressionsResponseSuccess.class); + } + + /** + * Removes up to 100 suppressions from the suppression list at once. + * Provide either emails or ids, but not both. + * + * @param removeSuppressionsOptions The suppressions to remove. + * @return The RemoveSuppressionsResponseSuccess with the details of the removed suppressions. + * @throws ResendException If an error occurs during the suppressions removal process. + */ + public RemoveSuppressionsResponseSuccess remove(RemoveSuppressionsOptions removeSuppressionsOptions) throws ResendException { + String payload = super.resendMapper.writeValue(removeSuppressionsOptions); + AbstractHttpResponse response = httpClient.perform("/suppressions/batch/remove", super.apiKey, HttpMethod.POST, payload, MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + String responseBody = response.getBody(); + + return resendMapper.readValue(responseBody, RemoveSuppressionsResponseSuccess.class); + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/AddSuppressionOptions.java b/src/main/java/com/resend/services/suppressions/model/AddSuppressionOptions.java new file mode 100644 index 0000000..54ff1f2 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/AddSuppressionOptions.java @@ -0,0 +1,72 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a request to add an email address to the suppression list. + */ +public class AddSuppressionOptions { + + @JsonProperty("email") + private final String email; + + /** + * Constructs an Add Suppression Options object using the provided builder. + * + * @param builder The builder to construct the Add Suppression Options. + */ + public AddSuppressionOptions(Builder builder) { + this.email = builder.email; + } + + /** + * Get the email address to suppress. + * + * @return The email address to suppress. + */ + public String getEmail() { + return email; + } + + /** + * Create a new builder instance for constructing AddSuppressionOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing AddSuppressionOptions objects. + */ + public static class Builder { + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + private String email; + + /** + * Set the email address to suppress. + * + * @param email The email address to suppress. + * @return The builder instance. + */ + public Builder email(String email) { + this.email = email; + return this; + } + + /** + * Build a new AddSuppressionOptions object. + * + * @return A new AddSuppressionOptions object. + */ + public AddSuppressionOptions build() { + return new AddSuppressionOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java new file mode 100644 index 0000000..fa0ad60 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java @@ -0,0 +1,51 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for adding an email address to the suppression list. + */ +public class AddSuppressionResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public AddSuppressionResponseSuccess() { + + } + + /** + * Constructs a successful response for adding a suppression. + * + * @param object The object type of the suppression. + * @param id The ID of the suppression. + */ + public AddSuppressionResponseSuccess(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. + * + * @return The object type of the suppression. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the suppression. + * + * @return The ID of the suppression. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java b/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java new file mode 100644 index 0000000..ea2e95c --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java @@ -0,0 +1,90 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a request to add email addresses to the suppression list. + * Must contain between 1 and 100 email addresses. + */ +public class AddSuppressionsOptions { + + @JsonProperty("emails") + private final List emails; + + /** + * Constructs an Add Suppressions Options object using the provided builder. + * + * @param builder The builder to construct the Add Suppressions Options. + */ + public AddSuppressionsOptions(Builder builder) { + this.emails = builder.emails; + } + + /** + * Get the email addresses to suppress. + * + * @return The email addresses to suppress. + */ + public List getEmails() { + return emails; + } + + /** + * Create a new builder instance for constructing AddSuppressionsOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing AddSuppressionsOptions objects. + */ + public static class Builder { + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + private List emails; + + /** + * Set the email addresses to suppress. + * + * @param emails The email addresses to suppress. + * @return The builder instance. + */ + public Builder emails(List emails) { + this.emails = emails; + return this; + } + + /** + * Add an email address to suppress. + * + * @param email The email address to suppress. + * @return The builder instance. + */ + public Builder email(String email) { + if (this.emails == null) { + this.emails = new ArrayList<>(); + } + this.emails.add(email); + return this; + } + + /** + * Build a new AddSuppressionsOptions object. + * + * @return A new AddSuppressionsOptions object. + */ + public AddSuppressionsOptions build() { + return new AddSuppressionsOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/AddSuppressionsResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/AddSuppressionsResponseSuccess.java new file mode 100644 index 0000000..d842407 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/AddSuppressionsResponseSuccess.java @@ -0,0 +1,39 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for adding email addresses to the suppression list. + */ +public class AddSuppressionsResponseSuccess { + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public AddSuppressionsResponseSuccess() { + + } + + /** + * Constructs a successful response for adding suppressions. + * + * @param data The list of added suppressions. + */ + public AddSuppressionsResponseSuccess(List data) { + this.data = data; + } + + /** + * Get the list of added suppressions. + * + * @return The list of added suppressions. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/AddedSuppression.java b/src/main/java/com/resend/services/suppressions/model/AddedSuppression.java new file mode 100644 index 0000000..2a38580 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/AddedSuppression.java @@ -0,0 +1,51 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a suppression added to the suppression list. + */ +public class AddedSuppression { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + /** + * Default constructor + */ + public AddedSuppression() { + + } + + /** + * Constructs an added suppression. + * + * @param object The object type of the suppression. + * @param id The ID of the suppression. + */ + public AddedSuppression(String object, String id) { + this.object = object; + this.id = id; + } + + /** + * Get the object type. + * + * @return The object type of the suppression. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the suppression. + * + * @return The ID of the suppression. + */ + public String getId() { + return id; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java new file mode 100644 index 0000000..0203eb6 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java @@ -0,0 +1,108 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for retrieving a suppression. + */ +public class GetSuppressionResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("email") + private String email; + + @JsonProperty("origin") + private String origin; + + @JsonProperty("source_id") + private String sourceId; + + @JsonProperty("created_at") + private String createdAt; + + /** + * Default constructor + */ + public GetSuppressionResponseSuccess() { + + } + + /** + * Constructs a successful response for retrieving a suppression. + * + * @param object The object type of the suppression. + * @param id The ID of the suppression. + * @param email The suppressed email address. + * @param origin The origin of the suppression. + * @param sourceId The ID of the email that triggered the suppression. + * @param createdAt The creation timestamp of the suppression. + */ + public GetSuppressionResponseSuccess(String object, String id, String email, String origin, String sourceId, String createdAt) { + this.object = object; + this.id = id; + this.email = email; + this.origin = origin; + this.sourceId = sourceId; + this.createdAt = createdAt; + } + + /** + * Get the object type. + * + * @return The object type of the suppression. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the suppression. + * + * @return The ID of the suppression. + */ + public String getId() { + return id; + } + + /** + * Get the suppressed email address. + * + * @return The suppressed email address. + */ + public String getEmail() { + return email; + } + + /** + * Get the origin of the suppression. + * + * @return The origin of the suppression. + */ + public String getOrigin() { + return origin; + } + + /** + * Get the ID of the email that triggered the suppression. + * For suppressions with a manual origin, the source ID is null. + * + * @return The ID of the email that triggered the suppression. + */ + public String getSourceId() { + return sourceId; + } + + /** + * Get the creation timestamp of the suppression. + * + * @return The creation timestamp of the suppression. + */ + public String getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java b/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java new file mode 100644 index 0000000..449105b --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java @@ -0,0 +1,172 @@ +package com.resend.services.suppressions.model; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Represents parameters for listing suppressions with filtering and pagination. + */ +public class ListSuppressionsParams { + + private final Integer limit; + private final String after; + private final String before; + private final SuppressionOrigin origin; + + /** + * Constructs ListSuppressionsParams using the provided builder. + * + * @param builder The builder to construct the params. + */ + public ListSuppressionsParams(Builder builder) { + this.limit = builder.limit; + this.after = builder.after; + this.before = builder.before; + this.origin = builder.origin; + } + + /** + * Retrieves the maximum number of results. + * + * @return The limit. + */ + public Integer getLimit() { + return limit; + } + + /** + * Retrieves the cursor for pagination (after). + * + * @return The after cursor. + */ + public String getAfter() { + return after; + } + + /** + * Retrieves the cursor for pagination (before). + * + * @return The before cursor. + */ + public String getBefore() { + return before; + } + + /** + * Retrieves the origin filter. + * + * @return The suppression origin filter. + */ + public SuppressionOrigin getOrigin() { + return origin; + } + + /** + * Converts the parameters to a query string. + * + * @return A query string starting with "?" if parameters exist, or an empty string otherwise. + */ + public String toQueryString() { + Map queryParams = new LinkedHashMap<>(); + + if (limit != null) { + queryParams.put("limit", limit.toString()); + } + if (after != null && !after.isEmpty()) { + queryParams.put("after", after); + } + if (before != null && !before.isEmpty()) { + queryParams.put("before", before); + } + if (origin != null) { + queryParams.put("origin", origin.getValue()); + } + + if (queryParams.isEmpty()) { + return ""; + } + + return "?" + queryParams.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + + /** + * Creates a new builder instance for ListSuppressionsParams. + * + * @return A new Builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing ListSuppressionsParams objects. + */ + public static class Builder { + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + private Integer limit; + private String after; + private String before; + private SuppressionOrigin origin; + + /** + * Sets the maximum number of results. + * + * @param limit The limit. + * @return The builder instance. + */ + public Builder limit(Integer limit) { + this.limit = limit; + return this; + } + + /** + * Sets the after cursor for pagination. + * + * @param after The after cursor. + * @return The builder instance. + */ + public Builder after(String after) { + this.after = after; + return this; + } + + /** + * Sets the before cursor for pagination. + * + * @param before The before cursor. + * @return The builder instance. + */ + public Builder before(String before) { + this.before = before; + return this; + } + + /** + * Sets the origin filter. + * + * @param origin The suppression origin. + * @return The builder instance. + */ + public Builder origin(SuppressionOrigin origin) { + this.origin = origin; + return this; + } + + /** + * Builds a new ListSuppressionsParams instance. + * + * @return A new ListSuppressionsParams. + */ + public ListSuppressionsParams build() { + return new ListSuppressionsParams(this); + } + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/ListSuppressionsResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/ListSuppressionsResponseSuccess.java new file mode 100644 index 0000000..e13cdcd --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/ListSuppressionsResponseSuccess.java @@ -0,0 +1,67 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for listing suppressions. + */ +public class ListSuppressionsResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("has_more") + private Boolean hasMore; + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public ListSuppressionsResponseSuccess() { + + } + + /** + * Constructs a successful response for listing suppressions. + * + * @param object The object type of the list. + * @param hasMore Whether there are more suppressions available for pagination. + * @param data The list of suppressions. + */ + public ListSuppressionsResponseSuccess(String object, Boolean hasMore, List data) { + this.object = object; + this.hasMore = hasMore; + this.data = data; + } + + /** + * Get the object type. + * + * @return The object type of the list. + */ + public String getObject() { + return object; + } + + /** + * Get the indicator whether there are more suppressions available for pagination. + * + * @return Whether there are more suppressions available for pagination. + */ + public Boolean hasMore() { + return hasMore; + } + + /** + * Get the list of suppressions. + * + * @return The list of suppressions. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java new file mode 100644 index 0000000..7f1231e --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java @@ -0,0 +1,65 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response for removing a suppression from the suppression list. + */ +public class RemoveSuppressionResponseSuccess { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("deleted") + private Boolean deleted; + + /** + * Default constructor + */ + public RemoveSuppressionResponseSuccess() { + + } + + /** + * Constructs a successful response for removing a suppression. + * + * @param object The object type of the suppression. + * @param id The ID of the suppression. + * @param deleted Whether the suppression was deleted. + */ + public RemoveSuppressionResponseSuccess(String object, String id, Boolean deleted) { + this.object = object; + this.id = id; + this.deleted = deleted; + } + + /** + * Get the object type. + * + * @return The object type of the suppression. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the suppression. + * + * @return The ID of the suppression. + */ + public String getId() { + return id; + } + + /** + * Get whether the suppression was deleted. + * + * @return Whether the suppression was deleted. + */ + public Boolean getDeleted() { + return deleted; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java new file mode 100644 index 0000000..934061f --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java @@ -0,0 +1,131 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a request to remove suppressions from the suppression list. + * Provide either emails or ids, but not both. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RemoveSuppressionsOptions { + + @JsonProperty("emails") + private final List emails; + + @JsonProperty("ids") + private final List ids; + + /** + * Constructs a Remove Suppressions Options object using the provided builder. + * + * @param builder The builder to construct the Remove Suppressions Options. + */ + public RemoveSuppressionsOptions(Builder builder) { + this.emails = builder.emails; + this.ids = builder.ids; + } + + /** + * Get the email addresses to remove from the suppression list. + * + * @return The email addresses to remove from the suppression list. + */ + public List getEmails() { + return emails; + } + + /** + * Get the suppression IDs to remove from the suppression list. + * + * @return The suppression IDs to remove from the suppression list. + */ + public List getIds() { + return ids; + } + + /** + * Create a new builder instance for constructing RemoveSuppressionsOptions objects. + * + * @return A new builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing RemoveSuppressionsOptions objects. + */ + public static class Builder { + /** + * Creates a new Builder instance. + */ + public Builder() { + } + + private List emails; + private List ids; + + /** + * Set the email addresses to remove from the suppression list. + * + * @param emails The email addresses to remove from the suppression list. + * @return The builder instance. + */ + public Builder emails(List emails) { + this.emails = emails; + return this; + } + + /** + * Add an email address to remove from the suppression list. + * + * @param email The email address to remove from the suppression list. + * @return The builder instance. + */ + public Builder email(String email) { + if (this.emails == null) { + this.emails = new ArrayList<>(); + } + this.emails.add(email); + return this; + } + + /** + * Set the suppression IDs to remove from the suppression list. + * + * @param ids The suppression IDs to remove from the suppression list. + * @return The builder instance. + */ + public Builder ids(List ids) { + this.ids = ids; + return this; + } + + /** + * Add a suppression ID to remove from the suppression list. + * + * @param id The suppression ID to remove from the suppression list. + * @return The builder instance. + */ + public Builder id(String id) { + if (this.ids == null) { + this.ids = new ArrayList<>(); + } + this.ids.add(id); + return this; + } + + /** + * Build a new RemoveSuppressionsOptions object. + * + * @return A new RemoveSuppressionsOptions object. + */ + public RemoveSuppressionsOptions build() { + return new RemoveSuppressionsOptions(this); + } + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsResponseSuccess.java new file mode 100644 index 0000000..f1c04aa --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsResponseSuccess.java @@ -0,0 +1,39 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Represents a successful response for removing suppressions from the suppression list. + */ +public class RemoveSuppressionsResponseSuccess { + + @JsonProperty("data") + private List data; + + /** + * Default constructor + */ + public RemoveSuppressionsResponseSuccess() { + + } + + /** + * Constructs a successful response for removing suppressions. + * + * @param data The list of removed suppressions. + */ + public RemoveSuppressionsResponseSuccess(List data) { + this.data = data; + } + + /** + * Get the list of removed suppressions. + * + * @return The list of removed suppressions. + */ + public List getData() { + return data; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/RemovedSuppression.java b/src/main/java/com/resend/services/suppressions/model/RemovedSuppression.java new file mode 100644 index 0000000..46ff023 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/RemovedSuppression.java @@ -0,0 +1,65 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a suppression removed from the suppression list. + */ +public class RemovedSuppression { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("deleted") + private Boolean deleted; + + /** + * Default constructor + */ + public RemovedSuppression() { + + } + + /** + * Constructs a removed suppression. + * + * @param object The object type of the suppression. + * @param id The ID of the suppression. + * @param deleted Whether the suppression was deleted. + */ + public RemovedSuppression(String object, String id, Boolean deleted) { + this.object = object; + this.id = id; + this.deleted = deleted; + } + + /** + * Get the object type. + * + * @return The object type of the suppression. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the suppression. + * + * @return The ID of the suppression. + */ + public String getId() { + return id; + } + + /** + * Get whether the suppression was deleted. + * + * @return Whether the suppression was deleted. + */ + public Boolean getDeleted() { + return deleted; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/Suppression.java b/src/main/java/com/resend/services/suppressions/model/Suppression.java new file mode 100644 index 0000000..5d87322 --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/Suppression.java @@ -0,0 +1,108 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a suppression from the suppression list. + */ +public class Suppression { + + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("email") + private String email; + + @JsonProperty("origin") + private String origin; + + @JsonProperty("source_id") + private String sourceId; + + @JsonProperty("created_at") + private String createdAt; + + /** + * Default constructor + */ + public Suppression() { + + } + + /** + * Constructs a suppression. + * + * @param object The object type of the suppression. + * @param id The ID of the suppression. + * @param email The suppressed email address. + * @param origin The origin of the suppression. + * @param sourceId The ID of the email that triggered the suppression. + * @param createdAt The creation timestamp of the suppression. + */ + public Suppression(String object, String id, String email, String origin, String sourceId, String createdAt) { + this.object = object; + this.id = id; + this.email = email; + this.origin = origin; + this.sourceId = sourceId; + this.createdAt = createdAt; + } + + /** + * Get the object type. + * + * @return The object type of the suppression. + */ + public String getObject() { + return object; + } + + /** + * Get the ID of the suppression. + * + * @return The ID of the suppression. + */ + public String getId() { + return id; + } + + /** + * Get the suppressed email address. + * + * @return The suppressed email address. + */ + public String getEmail() { + return email; + } + + /** + * Get the origin of the suppression. + * + * @return The origin of the suppression. + */ + public String getOrigin() { + return origin; + } + + /** + * Get the ID of the email that triggered the suppression. + * For suppressions with a manual origin, the source ID is null. + * + * @return The ID of the email that triggered the suppression. + */ + public String getSourceId() { + return sourceId; + } + + /** + * Get the creation timestamp of the suppression. + * + * @return The creation timestamp of the suppression. + */ + public String getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/com/resend/services/suppressions/model/SuppressionOrigin.java b/src/main/java/com/resend/services/suppressions/model/SuppressionOrigin.java new file mode 100644 index 0000000..c44c73f --- /dev/null +++ b/src/main/java/com/resend/services/suppressions/model/SuppressionOrigin.java @@ -0,0 +1,49 @@ +package com.resend.services.suppressions.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Represents the origin of a suppression. + */ +public enum SuppressionOrigin { + /** Emails suppressed automatically after a bounce. */ + BOUNCE("bounce"), + /** Emails suppressed due to a user complaint. */ + COMPLAINT("complaint"), + /** Emails suppressed by your team manually. */ + MANUAL("manual"); + + private final String value; + + SuppressionOrigin(String value) { + this.value = value; + } + + /** + * Returns the string value of the origin. + * + * @return The origin value. + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Creates a SuppressionOrigin from a string value. + * + * @param value The string value. + * @return The corresponding SuppressionOrigin. + * @throws IllegalArgumentException If the value is unknown. + */ + @JsonCreator + public static SuppressionOrigin fromValue(String value) { + for (SuppressionOrigin origin : SuppressionOrigin.values()) { + if (origin.value.equals(value)) { + return origin; + } + } + throw new IllegalArgumentException("Unknown origin: " + value); + } +} diff --git a/src/test/java/com/resend/services/suppressions/SuppressionsTest.java b/src/test/java/com/resend/services/suppressions/SuppressionsTest.java new file mode 100644 index 0000000..81ab473 --- /dev/null +++ b/src/test/java/com/resend/services/suppressions/SuppressionsTest.java @@ -0,0 +1,315 @@ +package com.resend.services.suppressions; + +import com.resend.core.exception.ResendException; +import com.resend.core.net.AbstractHttpResponse; +import com.resend.core.net.HttpMethod; +import com.resend.core.net.IHttpClient; +import com.resend.services.suppressions.model.AddSuppressionOptions; +import com.resend.services.suppressions.model.AddSuppressionResponseSuccess; +import com.resend.services.suppressions.model.AddSuppressionsOptions; +import com.resend.services.suppressions.model.AddSuppressionsResponseSuccess; +import com.resend.services.suppressions.model.GetSuppressionResponseSuccess; +import com.resend.services.suppressions.model.ListSuppressionsParams; +import com.resend.services.suppressions.model.ListSuppressionsResponseSuccess; +import com.resend.services.suppressions.model.RemoveSuppressionResponseSuccess; +import com.resend.services.suppressions.model.RemoveSuppressionsOptions; +import com.resend.services.suppressions.model.RemoveSuppressionsResponseSuccess; +import com.resend.services.suppressions.model.SuppressionOrigin; +import com.resend.services.util.SuppressionsUtil; +import okhttp3.MediaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") +public class SuppressionsTest { + + private static final String ADD_SINGLE_RESPONSE_JSON = + "{\"object\":\"suppression\",\"id\":\"e169aa45-1ecf-4183-9955-b1499d5701d3\"}"; + + private static final String REMOVE_SINGLE_RESPONSE_JSON = + "{\"object\":\"suppression\",\"id\":\"e169aa45-1ecf-4183-9955-b1499d5701d3\",\"deleted\":true}"; + + private static final String ADD_RESPONSE_JSON = "{\"data\":[" + + "{\"object\":\"suppression\",\"id\":\"e169aa45-1ecf-4183-9955-b1499d5701d3\"}," + + "{\"object\":\"suppression\",\"id\":\"520784e2-887d-4c25-b53c-4ad46ad38100\"}" + + "]}"; + + private static final String GET_RESPONSE_JSON = + "{\"object\":\"suppression\",\"id\":\"e169aa45-1ecf-4183-9955-b1499d5701d3\"," + + "\"email\":\"steve.wozniak@example.com\",\"origin\":\"bounce\"," + + "\"source_id\":\"4ef9a417-02e9-4d39-ad75-9611e0fcc33c\"," + + "\"created_at\":\"2026-10-06T23:47:56.678Z\"}"; + + private static final String LIST_RESPONSE_JSON = "{\"object\":\"list\",\"has_more\":false,\"data\":[" + + "{\"object\":\"suppression\",\"id\":\"e169aa45-1ecf-4183-9955-b1499d5701d3\"," + + "\"email\":\"steve.wozniak@example.com\",\"origin\":\"manual\",\"source_id\":null," + + "\"created_at\":\"2026-10-06T23:47:56.678Z\"}," + + "{\"object\":\"suppression\",\"id\":\"520784e2-887d-4c25-b53c-4ad46ad38100\"," + + "\"email\":\"susan.kare@example.com\",\"origin\":\"bounce\"," + + "\"source_id\":\"4ef9a417-02e9-4d39-ad75-9611e0fcc33c\"," + + "\"created_at\":\"2026-10-07T08:12:03.412Z\"}" + + "]}"; + + private static final String REMOVE_RESPONSE_JSON = "{\"data\":[" + + "{\"object\":\"suppression\",\"id\":\"e169aa45-1ecf-4183-9955-b1499d5701d3\",\"deleted\":true}" + + "]}"; + + @Mock + private IHttpClient httpClient; + + private Suppressions suppressions; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + suppressions = new Suppressions("test-api-key", httpClient); + } + + @Test + public void testAddSuppression_Success() throws ResendException { + AddSuppressionOptions request = SuppressionsUtil.addSuppressionRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, ADD_SINGLE_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + AddSuppressionResponseSuccess response = suppressions.add(request); + + assertNotNull(response); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getId()); + assertEquals("suppression", response.getObject()); + } + + @Test + public void testAddSuppression_ApiError_ThrowsResendException() throws ResendException { + AddSuppressionOptions request = SuppressionsUtil.addSuppressionRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(422, + "{\"name\":\"validation_error\",\"message\":\"Invalid request\"}", false); + + when(httpClient.perform(eq("/suppressions"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + ResendException ex = assertThrows(ResendException.class, () -> suppressions.add(request)); + assertEquals(422, (int) ex.getStatusCode()); + } + + @Test + public void testRemoveSuppressionById_Success() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, REMOVE_SINGLE_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/e169aa45-1ecf-4183-9955-b1499d5701d3"), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + RemoveSuppressionResponseSuccess response = suppressions.remove("e169aa45-1ecf-4183-9955-b1499d5701d3"); + + assertNotNull(response); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getId()); + assertEquals("suppression", response.getObject()); + assertTrue(response.getDeleted()); + } + + @Test + public void testRemoveSuppressionByEmail_Success() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, REMOVE_SINGLE_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/steve.wozniak@example.com"), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + RemoveSuppressionResponseSuccess response = suppressions.remove("steve.wozniak@example.com"); + + assertNotNull(response); + assertTrue(response.getDeleted()); + } + + @Test + public void testRemoveSuppression_ApiError_ThrowsResendException() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(404, + "{\"name\":\"not_found\",\"message\":\"Suppression not found\"}", false); + + when(httpClient.perform(eq("/suppressions/123"), anyString(), eq(HttpMethod.DELETE), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + assertThrows(ResendException.class, () -> suppressions.remove("123")); + } + + @Test + public void testAddSuppressions_Success() throws ResendException { + AddSuppressionsOptions request = SuppressionsUtil.addSuppressionsRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, ADD_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/batch/add"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + AddSuppressionsResponseSuccess response = suppressions.batch().add(request); + + assertNotNull(response); + assertEquals(2, response.getData().size()); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getData().get(0).getId()); + assertEquals("suppression", response.getData().get(0).getObject()); + assertEquals("520784e2-887d-4c25-b53c-4ad46ad38100", response.getData().get(1).getId()); + } + + @Test + public void testAddSuppressions_ApiError_ThrowsResendException() throws ResendException { + AddSuppressionsOptions request = SuppressionsUtil.addSuppressionsRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(422, + "{\"name\":\"validation_error\",\"message\":\"Invalid request\"}", false); + + when(httpClient.perform(eq("/suppressions/batch/add"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + ResendException ex = assertThrows(ResendException.class, () -> suppressions.batch().add(request)); + assertEquals(422, (int) ex.getStatusCode()); + } + + @Test + public void testGetSuppressionById_Success() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, GET_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/e169aa45-1ecf-4183-9955-b1499d5701d3"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + GetSuppressionResponseSuccess response = suppressions.get("e169aa45-1ecf-4183-9955-b1499d5701d3"); + + assertNotNull(response); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getId()); + assertEquals("steve.wozniak@example.com", response.getEmail()); + assertEquals("bounce", response.getOrigin()); + assertEquals("4ef9a417-02e9-4d39-ad75-9611e0fcc33c", response.getSourceId()); + assertEquals("2026-10-06T23:47:56.678Z", response.getCreatedAt()); + assertEquals("suppression", response.getObject()); + } + + @Test + public void testGetSuppressionByEmail_Success() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, GET_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/steve.wozniak@example.com"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + GetSuppressionResponseSuccess response = suppressions.get("steve.wozniak@example.com"); + + assertNotNull(response); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getId()); + assertEquals("steve.wozniak@example.com", response.getEmail()); + } + + @Test + public void testGetSuppression_ApiError_ThrowsResendException() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(404, + "{\"name\":\"not_found\",\"message\":\"Suppression not found\"}", false); + + when(httpClient.perform(eq("/suppressions/123"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + assertThrows(ResendException.class, () -> suppressions.get("123")); + } + + @Test + public void testListSuppressions_Success() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, LIST_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + ListSuppressionsResponseSuccess response = suppressions.list(); + + assertNotNull(response); + assertEquals("list", response.getObject()); + assertFalse(response.hasMore()); + assertEquals(2, response.getData().size()); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getData().get(0).getId()); + assertEquals("steve.wozniak@example.com", response.getData().get(0).getEmail()); + assertEquals("manual", response.getData().get(0).getOrigin()); + assertNull(response.getData().get(0).getSourceId()); + assertEquals("bounce", response.getData().get(1).getOrigin()); + assertEquals("4ef9a417-02e9-4d39-ad75-9611e0fcc33c", response.getData().get(1).getSourceId()); + } + + @Test + public void testListSuppressionsWithParams_Success() throws ResendException { + ListSuppressionsParams params = SuppressionsUtil.listSuppressionsParams(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, LIST_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions?limit=20&origin=bounce"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + ListSuppressionsResponseSuccess response = suppressions.list(params); + + assertNotNull(response); + assertEquals(2, response.getData().size()); + } + + @Test + public void testListSuppressionsParams_ToQueryString() { + ListSuppressionsParams params = ListSuppressionsParams.builder() + .limit(50) + .after("cursor_abc") + .origin(SuppressionOrigin.MANUAL) + .build(); + + assertEquals("?limit=50&after=cursor_abc&origin=manual", params.toQueryString()); + } + + @Test + public void testListSuppressions_ApiError_ThrowsResendException() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(401, + "{\"name\":\"missing_api_key\",\"message\":\"Missing API key\"}", false); + + when(httpClient.perform(eq("/suppressions"), anyString(), eq(HttpMethod.GET), isNull(), any(MediaType.class))) + .thenReturn(httpResponse); + + assertThrows(ResendException.class, () -> suppressions.list()); + } + + @Test + public void testRemoveSuppressionsByEmails_Success() throws ResendException { + RemoveSuppressionsOptions request = SuppressionsUtil.removeSuppressionsByEmailsRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, REMOVE_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/batch/remove"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + RemoveSuppressionsResponseSuccess response = suppressions.batch().remove(request); + + assertNotNull(response); + assertEquals(1, response.getData().size()); + assertEquals("e169aa45-1ecf-4183-9955-b1499d5701d3", response.getData().get(0).getId()); + assertEquals("suppression", response.getData().get(0).getObject()); + assertTrue(response.getData().get(0).getDeleted()); + } + + @Test + public void testRemoveSuppressionsByIds_Success() throws ResendException { + RemoveSuppressionsOptions request = SuppressionsUtil.removeSuppressionsByIdsRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, REMOVE_RESPONSE_JSON, true); + + when(httpClient.perform(eq("/suppressions/batch/remove"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + RemoveSuppressionsResponseSuccess response = suppressions.batch().remove(request); + + assertNotNull(response); + assertEquals(1, response.getData().size()); + assertTrue(response.getData().get(0).getDeleted()); + } + + @Test + public void testRemoveSuppressions_ApiError_ThrowsResendException() throws ResendException { + RemoveSuppressionsOptions request = SuppressionsUtil.removeSuppressionsByIdsRequest(); + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(422, + "{\"name\":\"validation_error\",\"message\":\"Invalid request\"}", false); + + when(httpClient.perform(eq("/suppressions/batch/remove"), anyString(), eq(HttpMethod.POST), anyString(), any(MediaType.class))) + .thenReturn(httpResponse); + + ResendException ex = assertThrows(ResendException.class, () -> suppressions.batch().remove(request)); + assertEquals(422, (int) ex.getStatusCode()); + } +} diff --git a/src/test/java/com/resend/services/util/SuppressionsUtil.java b/src/test/java/com/resend/services/util/SuppressionsUtil.java new file mode 100644 index 0000000..d53686d --- /dev/null +++ b/src/test/java/com/resend/services/util/SuppressionsUtil.java @@ -0,0 +1,92 @@ +package com.resend.services.util; + +import com.resend.services.suppressions.model.*; + +import java.util.ArrayList; +import java.util.List; + +public class SuppressionsUtil { + + public static AddSuppressionOptions addSuppressionRequest() { + return AddSuppressionOptions.builder() + .email("steve.wozniak@example.com") + .build(); + } + + public static AddSuppressionResponseSuccess addSuppressionResponse() { + return new AddSuppressionResponseSuccess("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3"); + } + + public static RemoveSuppressionResponseSuccess removeSuppressionResponse() { + return new RemoveSuppressionResponseSuccess("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3", true); + } + + public static AddSuppressionsOptions addSuppressionsRequest() { + return AddSuppressionsOptions.builder() + .email("steve.wozniak@example.com") + .email("susan.kare@example.com") + .build(); + } + + public static AddSuppressionsResponseSuccess addSuppressionsResponse() { + List data = new ArrayList<>(); + data.add(new AddedSuppression("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3")); + data.add(new AddedSuppression("suppression", "520784e2-887d-4c25-b53c-4ad46ad38100")); + return new AddSuppressionsResponseSuccess(data); + } + + public static GetSuppressionResponseSuccess getSuppressionResponse() { + return new GetSuppressionResponseSuccess( + "suppression", + "e169aa45-1ecf-4183-9955-b1499d5701d3", + "steve.wozniak@example.com", + "bounce", + "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + "2026-10-06T23:47:56.678Z"); + } + + public static ListSuppressionsParams listSuppressionsParams() { + return ListSuppressionsParams.builder() + .limit(20) + .origin(SuppressionOrigin.BOUNCE) + .build(); + } + + public static ListSuppressionsResponseSuccess listSuppressionsResponse() { + List data = new ArrayList<>(); + data.add(new Suppression( + "suppression", + "e169aa45-1ecf-4183-9955-b1499d5701d3", + "steve.wozniak@example.com", + "manual", + null, + "2026-10-06T23:47:56.678Z")); + data.add(new Suppression( + "suppression", + "520784e2-887d-4c25-b53c-4ad46ad38100", + "susan.kare@example.com", + "bounce", + "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", + "2026-10-07T08:12:03.412Z")); + return new ListSuppressionsResponseSuccess("list", false, data); + } + + public static RemoveSuppressionsOptions removeSuppressionsByEmailsRequest() { + return RemoveSuppressionsOptions.builder() + .email("steve.wozniak@example.com") + .email("susan.kare@example.com") + .build(); + } + + public static RemoveSuppressionsOptions removeSuppressionsByIdsRequest() { + return RemoveSuppressionsOptions.builder() + .id("e169aa45-1ecf-4183-9955-b1499d5701d3") + .build(); + } + + public static RemoveSuppressionsResponseSuccess removeSuppressionsResponse() { + List data = new ArrayList<>(); + data.add(new RemovedSuppression("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3", true)); + return new RemoveSuppressionsResponseSuccess(data); + } +} From 96b577cad6e8072e1658711d024b25806430728a Mon Sep 17 00:00:00 2001 From: Kewyn Akshlley Date: Thu, 23 Jul 2026 11:06:48 -0300 Subject: [PATCH 2/2] fix: reuse base models, add builder validation and query encoding in suppressions --- src/main/java/com/resend/Resend.java | 10 +++ .../model/AddSuppressionResponseSuccess.java | 32 +------ .../model/AddSuppressionsOptions.java | 7 ++ .../model/GetSuppressionResponseSuccess.java | 85 +------------------ .../model/ListSuppressionsParams.java | 35 +++----- .../RemoveSuppressionResponseSuccess.java | 45 +--------- .../model/RemoveSuppressionsOptions.java | 6 ++ .../suppressions/SuppressionsTest.java | 36 ++++++++ .../services/util/SuppressionsUtil.java | 53 ------------ 9 files changed, 81 insertions(+), 228 deletions(-) diff --git a/src/main/java/com/resend/Resend.java b/src/main/java/com/resend/Resend.java index 327890a..c3d21d1 100644 --- a/src/main/java/com/resend/Resend.java +++ b/src/main/java/com/resend/Resend.java @@ -10,6 +10,7 @@ import com.resend.services.domains.Domains; import com.resend.services.emails.Emails; import com.resend.services.segments.Segments; +import com.resend.services.suppressions.Suppressions; import com.resend.services.webhooks.Webhooks; import com.resend.services.receiving.Receiving; import com.resend.services.topics.Topics; @@ -183,6 +184,15 @@ public Automations automations() { return new Automations(apiKey); } + /** + * Returns a Suppressions object that can be used to interact with the Suppressions service. + * + * @return A Suppressions object. + */ + public Suppressions suppressions() { + return new Suppressions(apiKey); + } + /** * Returns an OAuthGrants object that can be used to interact with the OAuthGrants service. * diff --git a/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java index fa0ad60..1ba8d59 100644 --- a/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java +++ b/src/main/java/com/resend/services/suppressions/model/AddSuppressionResponseSuccess.java @@ -1,17 +1,10 @@ package com.resend.services.suppressions.model; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * Represents a successful response for adding an email address to the suppression list. + * Extends the AddedSuppression class. */ -public class AddSuppressionResponseSuccess { - - @JsonProperty("object") - private String object; - - @JsonProperty("id") - private String id; +public class AddSuppressionResponseSuccess extends AddedSuppression { /** * Default constructor @@ -27,25 +20,6 @@ public AddSuppressionResponseSuccess() { * @param id The ID of the suppression. */ public AddSuppressionResponseSuccess(String object, String id) { - this.object = object; - this.id = id; - } - - /** - * Get the object type. - * - * @return The object type of the suppression. - */ - public String getObject() { - return object; - } - - /** - * Get the ID of the suppression. - * - * @return The ID of the suppression. - */ - public String getId() { - return id; + super(object, id); } } diff --git a/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java b/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java index ea2e95c..78a4a0b 100644 --- a/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java +++ b/src/main/java/com/resend/services/suppressions/model/AddSuppressionsOptions.java @@ -82,8 +82,15 @@ public Builder email(String email) { * Build a new AddSuppressionsOptions object. * * @return A new AddSuppressionsOptions object. + * @throws IllegalArgumentException If emails is empty or contains more than 100 email addresses. */ public AddSuppressionsOptions build() { + if (emails == null || emails.isEmpty()) { + throw new IllegalArgumentException("At least one email address must be provided"); + } + if (emails.size() > 100) { + throw new IllegalArgumentException("A maximum of 100 email addresses can be provided"); + } return new AddSuppressionsOptions(this); } } diff --git a/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java index 0203eb6..fb5ce23 100644 --- a/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java +++ b/src/main/java/com/resend/services/suppressions/model/GetSuppressionResponseSuccess.java @@ -1,29 +1,10 @@ package com.resend.services.suppressions.model; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * Represents a successful response for retrieving a suppression. + * Extends the Suppression class. */ -public class GetSuppressionResponseSuccess { - - @JsonProperty("object") - private String object; - - @JsonProperty("id") - private String id; - - @JsonProperty("email") - private String email; - - @JsonProperty("origin") - private String origin; - - @JsonProperty("source_id") - private String sourceId; - - @JsonProperty("created_at") - private String createdAt; +public class GetSuppressionResponseSuccess extends Suppression { /** * Default constructor @@ -43,66 +24,6 @@ public GetSuppressionResponseSuccess() { * @param createdAt The creation timestamp of the suppression. */ public GetSuppressionResponseSuccess(String object, String id, String email, String origin, String sourceId, String createdAt) { - this.object = object; - this.id = id; - this.email = email; - this.origin = origin; - this.sourceId = sourceId; - this.createdAt = createdAt; - } - - /** - * Get the object type. - * - * @return The object type of the suppression. - */ - public String getObject() { - return object; - } - - /** - * Get the ID of the suppression. - * - * @return The ID of the suppression. - */ - public String getId() { - return id; - } - - /** - * Get the suppressed email address. - * - * @return The suppressed email address. - */ - public String getEmail() { - return email; - } - - /** - * Get the origin of the suppression. - * - * @return The origin of the suppression. - */ - public String getOrigin() { - return origin; - } - - /** - * Get the ID of the email that triggered the suppression. - * For suppressions with a manual origin, the source ID is null. - * - * @return The ID of the email that triggered the suppression. - */ - public String getSourceId() { - return sourceId; - } - - /** - * Get the creation timestamp of the suppression. - * - * @return The creation timestamp of the suppression. - */ - public String getCreatedAt() { - return createdAt; + super(object, id, email, origin, sourceId, createdAt); } } diff --git a/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java b/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java index 449105b..829791f 100644 --- a/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java +++ b/src/main/java/com/resend/services/suppressions/model/ListSuppressionsParams.java @@ -1,8 +1,10 @@ package com.resend.services.suppressions.model; -import java.util.LinkedHashMap; +import com.resend.core.helper.URLHelper; +import com.resend.core.net.ListParams; + +import java.util.Collections; import java.util.Map; -import java.util.stream.Collectors; /** * Represents parameters for listing suppressions with filtering and pagination. @@ -68,28 +70,17 @@ public SuppressionOrigin getOrigin() { * @return A query string starting with "?" if parameters exist, or an empty string otherwise. */ public String toQueryString() { - Map queryParams = new LinkedHashMap<>(); + ListParams base = ListParams.builder() + .limit(limit) + .after(after) + .before(before) + .build(); - if (limit != null) { - queryParams.put("limit", limit.toString()); - } - if (after != null && !after.isEmpty()) { - queryParams.put("after", after); - } - if (before != null && !before.isEmpty()) { - queryParams.put("before", before); - } - if (origin != null) { - queryParams.put("origin", origin.getValue()); - } - - if (queryParams.isEmpty()) { - return ""; - } + Map extras = origin == null + ? Collections.emptyMap() + : Collections.singletonMap("origin", origin.getValue()); - return "?" + queryParams.entrySet().stream() - .map(entry -> entry.getKey() + "=" + entry.getValue()) - .collect(Collectors.joining("&")); + return URLHelper.parse(base, extras); } /** diff --git a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java index 7f1231e..00e9ad9 100644 --- a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java +++ b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionResponseSuccess.java @@ -1,20 +1,10 @@ package com.resend.services.suppressions.model; -import com.fasterxml.jackson.annotation.JsonProperty; - /** * Represents a successful response for removing a suppression from the suppression list. + * Extends the RemovedSuppression class. */ -public class RemoveSuppressionResponseSuccess { - - @JsonProperty("object") - private String object; - - @JsonProperty("id") - private String id; - - @JsonProperty("deleted") - private Boolean deleted; +public class RemoveSuppressionResponseSuccess extends RemovedSuppression { /** * Default constructor @@ -31,35 +21,6 @@ public RemoveSuppressionResponseSuccess() { * @param deleted Whether the suppression was deleted. */ public RemoveSuppressionResponseSuccess(String object, String id, Boolean deleted) { - this.object = object; - this.id = id; - this.deleted = deleted; - } - - /** - * Get the object type. - * - * @return The object type of the suppression. - */ - public String getObject() { - return object; - } - - /** - * Get the ID of the suppression. - * - * @return The ID of the suppression. - */ - public String getId() { - return id; - } - - /** - * Get whether the suppression was deleted. - * - * @return Whether the suppression was deleted. - */ - public Boolean getDeleted() { - return deleted; + super(object, id, deleted); } } diff --git a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java index 934061f..a84b402 100644 --- a/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java +++ b/src/main/java/com/resend/services/suppressions/model/RemoveSuppressionsOptions.java @@ -123,8 +123,14 @@ public Builder id(String id) { * Build a new RemoveSuppressionsOptions object. * * @return A new RemoveSuppressionsOptions object. + * @throws IllegalArgumentException If neither or both of emails and ids are provided. */ public RemoveSuppressionsOptions build() { + boolean hasEmails = emails != null && !emails.isEmpty(); + boolean hasIds = ids != null && !ids.isEmpty(); + if (hasEmails == hasIds) { + throw new IllegalArgumentException("Either 'emails' or 'ids' must be provided, but not both."); + } return new RemoveSuppressionsOptions(this); } } diff --git a/src/test/java/com/resend/services/suppressions/SuppressionsTest.java b/src/test/java/com/resend/services/suppressions/SuppressionsTest.java index 81ab473..56ce3f0 100644 --- a/src/test/java/com/resend/services/suppressions/SuppressionsTest.java +++ b/src/test/java/com/resend/services/suppressions/SuppressionsTest.java @@ -300,6 +300,42 @@ public void testRemoveSuppressionsByIds_Success() throws ResendException { assertTrue(response.getData().get(0).getDeleted()); } + @Test + public void testAddSuppressionsOptions_EmptyEmails_ThrowsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> AddSuppressionsOptions.builder().build()); + } + + @Test + public void testAddSuppressionsOptions_MoreThan100Emails_ThrowsIllegalArgumentException() { + AddSuppressionsOptions.Builder builder = AddSuppressionsOptions.builder(); + for (int i = 0; i <= 100; i++) { + builder.email("user" + i + "@example.com"); + } + assertThrows(IllegalArgumentException.class, builder::build); + } + + @Test + public void testRemoveSuppressionsOptions_NeitherEmailsNorIds_ThrowsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> RemoveSuppressionsOptions.builder().build()); + } + + @Test + public void testRemoveSuppressionsOptions_BothEmailsAndIds_ThrowsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> RemoveSuppressionsOptions.builder() + .email("steve.wozniak@example.com") + .id("e169aa45-1ecf-4183-9955-b1499d5701d3") + .build()); + } + + @Test + public void testListSuppressionsParams_ToQueryString_EncodesValues() { + ListSuppressionsParams params = ListSuppressionsParams.builder() + .after("cursor&abc=def") + .build(); + + assertEquals("?after=cursor%26abc%3Ddef", params.toQueryString()); + } + @Test public void testRemoveSuppressions_ApiError_ThrowsResendException() throws ResendException { RemoveSuppressionsOptions request = SuppressionsUtil.removeSuppressionsByIdsRequest(); diff --git a/src/test/java/com/resend/services/util/SuppressionsUtil.java b/src/test/java/com/resend/services/util/SuppressionsUtil.java index d53686d..7edc5c0 100644 --- a/src/test/java/com/resend/services/util/SuppressionsUtil.java +++ b/src/test/java/com/resend/services/util/SuppressionsUtil.java @@ -2,9 +2,6 @@ import com.resend.services.suppressions.model.*; -import java.util.ArrayList; -import java.util.List; - public class SuppressionsUtil { public static AddSuppressionOptions addSuppressionRequest() { @@ -13,14 +10,6 @@ public static AddSuppressionOptions addSuppressionRequest() { .build(); } - public static AddSuppressionResponseSuccess addSuppressionResponse() { - return new AddSuppressionResponseSuccess("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3"); - } - - public static RemoveSuppressionResponseSuccess removeSuppressionResponse() { - return new RemoveSuppressionResponseSuccess("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3", true); - } - public static AddSuppressionsOptions addSuppressionsRequest() { return AddSuppressionsOptions.builder() .email("steve.wozniak@example.com") @@ -28,23 +17,6 @@ public static AddSuppressionsOptions addSuppressionsRequest() { .build(); } - public static AddSuppressionsResponseSuccess addSuppressionsResponse() { - List data = new ArrayList<>(); - data.add(new AddedSuppression("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3")); - data.add(new AddedSuppression("suppression", "520784e2-887d-4c25-b53c-4ad46ad38100")); - return new AddSuppressionsResponseSuccess(data); - } - - public static GetSuppressionResponseSuccess getSuppressionResponse() { - return new GetSuppressionResponseSuccess( - "suppression", - "e169aa45-1ecf-4183-9955-b1499d5701d3", - "steve.wozniak@example.com", - "bounce", - "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", - "2026-10-06T23:47:56.678Z"); - } - public static ListSuppressionsParams listSuppressionsParams() { return ListSuppressionsParams.builder() .limit(20) @@ -52,25 +24,6 @@ public static ListSuppressionsParams listSuppressionsParams() { .build(); } - public static ListSuppressionsResponseSuccess listSuppressionsResponse() { - List data = new ArrayList<>(); - data.add(new Suppression( - "suppression", - "e169aa45-1ecf-4183-9955-b1499d5701d3", - "steve.wozniak@example.com", - "manual", - null, - "2026-10-06T23:47:56.678Z")); - data.add(new Suppression( - "suppression", - "520784e2-887d-4c25-b53c-4ad46ad38100", - "susan.kare@example.com", - "bounce", - "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", - "2026-10-07T08:12:03.412Z")); - return new ListSuppressionsResponseSuccess("list", false, data); - } - public static RemoveSuppressionsOptions removeSuppressionsByEmailsRequest() { return RemoveSuppressionsOptions.builder() .email("steve.wozniak@example.com") @@ -83,10 +36,4 @@ public static RemoveSuppressionsOptions removeSuppressionsByIdsRequest() { .id("e169aa45-1ecf-4183-9955-b1499d5701d3") .build(); } - - public static RemoveSuppressionsResponseSuccess removeSuppressionsResponse() { - List data = new ArrayList<>(); - data.add(new RemovedSuppression("suppression", "e169aa45-1ecf-4183-9955-b1499d5701d3", true)); - return new RemoveSuppressionsResponseSuccess(data); - } }