diff --git a/src/main/java/org/zendesk/client/v2/IdempotencyUtil.java b/src/main/java/org/zendesk/client/v2/IdempotencyUtil.java
new file mode 100644
index 00000000..ed4a1f49
--- /dev/null
+++ b/src/main/java/org/zendesk/client/v2/IdempotencyUtil.java
@@ -0,0 +1,117 @@
+package org.zendesk.client.v2;
+
+import java.util.Optional;
+import org.asynchttpclient.AsyncCompletionHandler;
+import org.asynchttpclient.Request;
+import org.asynchttpclient.Response;
+import org.zendesk.client.v2.model.IdempotencyState;
+import org.zendesk.client.v2.model.IdempotencyState.Status;
+import org.zendesk.client.v2.model.IdempotentEntity;
+
+/**
+ * Utility class for handling Zendesk API idempotency keys.
+ *
+ * Provides methods to add idempotency headers to requests and process idempotency-related
+ * response headers. Supports the Zendesk API's idempotency feature which allows safe retries of
+ * create operations without creating duplicate resources.
+ *
+ * @see
+ * Zendesk API Idempotency
+ * @since 1.5.0
+ */
+public class IdempotencyUtil {
+
+ static final String IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
+ static final String IDEMPOTENCY_LOOKUP_HEADER = "x-idempotency-lookup";
+ static final String IDEMPOTENCY_LOOKUP_HIT = "hit";
+ static final String IDEMPOTENCY_LOOKUP_MISS = "miss";
+ static final String IDEMPOTENCY_ERROR_NAME = "IdempotentRequestError";
+
+ /**
+ * Adds an idempotency key header to the request if the state is present and pending.
+ *
+ * @param request the HTTP request to modify
+ * @param state the idempotency state, or null if idempotency is not being used
+ * @return a new request with the idempotency key header added, or the original request if state
+ * is null
+ * @throws IllegalArgumentException if the state is not in PENDING status
+ */
+ public static Request addIdempotencyState(Request request, IdempotencyState state) {
+ if (state == null) {
+ return request;
+ }
+
+ if (state.getStatus() != Status.PENDING) {
+ throw new IllegalArgumentException("Idempotency state must be PENDING to add to a request");
+ }
+
+ // https://developer.zendesk.com/api-reference/ticketing/introduction/#idempotency
+ return request.toBuilder()
+ .setHeader(IDEMPOTENCY_KEY_HEADER, state.getIdempotencyKey())
+ .build();
+ }
+
+ /**
+ * Wraps an async completion handler to process idempotency response headers.
+ *
+ *
The wrapped handler will automatically update the entity's idempotency fields based on the
+ * response headers returned by the Zendesk API. If the {@code x-idempotency-lookup} header
+ * indicates a "hit", the entity will be marked as previously created. If "miss", it will be
+ * marked as newly created.
+ *
+ * @param the entity type that implements IdempotentEntity
+ * @param handler the original async completion handler
+ * @param idempotencyState the idempotency state, or null if idempotency is not being used
+ * @return a wrapped handler that processes idempotency headers, or the original handler if state
+ * is null
+ */
+ public static AsyncCompletionHandler wrapHandler(
+ AsyncCompletionHandler handler,
+ IdempotencyState idempotencyState) {
+ if (idempotencyState == null) {
+ return handler;
+ }
+
+ return new AsyncCompletionHandler<>() {
+ @Override
+ public T onCompleted(Response response) throws Exception {
+ T entity = handler.onCompleted(response);
+ transitionIdempotencyState(idempotencyState, response)
+ .ifPresent(newState -> newState.apply(entity));
+ return entity;
+ }
+
+ @Override
+ public void onThrowable(Throwable t) {
+ handler.onThrowable(t);
+ }
+ };
+ }
+
+ private static Optional transitionIdempotencyState(
+ IdempotencyState state,
+ Response response) {
+ if (state == null) {
+ return Optional.empty();
+ }
+
+ // https://developer.zendesk.com/api-reference/ticketing/introduction/#idempotency
+ String idempotencyLookup = response.getHeader(IDEMPOTENCY_LOOKUP_HEADER);
+ if (idempotencyLookup == null) {
+ return Optional.empty();
+ }
+
+ switch (idempotencyLookup) {
+ case IDEMPOTENCY_LOOKUP_HIT:
+ return Optional.of(state.toPreviouslyCreated());
+ case IDEMPOTENCY_LOOKUP_MISS:
+ return Optional.of(state.toCreated());
+ default:
+ return Optional.empty();
+ }
+ }
+
+ private IdempotencyUtil() {
+ throw new UnsupportedOperationException("Utility class");
+ }
+}
diff --git a/src/main/java/org/zendesk/client/v2/Zendesk.java b/src/main/java/org/zendesk/client/v2/Zendesk.java
index 968bed1e..c4e82feb 100644
--- a/src/main/java/org/zendesk/client/v2/Zendesk.java
+++ b/src/main/java/org/zendesk/client/v2/Zendesk.java
@@ -56,6 +56,8 @@
import org.zendesk.client.v2.model.Forum;
import org.zendesk.client.v2.model.Group;
import org.zendesk.client.v2.model.GroupMembership;
+import org.zendesk.client.v2.model.IdempotencyState;
+import org.zendesk.client.v2.model.IdempotentEntity;
import org.zendesk.client.v2.model.Identity;
import org.zendesk.client.v2.model.JiraLink;
import org.zendesk.client.v2.model.JobStatus;
@@ -432,9 +434,15 @@ public ListenableFuture queueCreateTicketAsync(Ticket ticket) {
}
public ListenableFuture createTicketAsync(Ticket ticket) {
+ IdempotencyState idempotencyState = IdempotencyState.of(ticket).orElse(null);
return submit(
- req("POST", cnst("/tickets.json"), JSON, json(Collections.singletonMap("ticket", ticket))),
- handle(Ticket.class, "ticket"));
+ req(
+ "POST",
+ cnst("/tickets.json"),
+ JSON,
+ json(Collections.singletonMap("ticket", ticket))),
+ handle(Ticket.class, "ticket"),
+ idempotencyState);
}
public Ticket createTicket(Ticket ticket) {
@@ -3474,8 +3482,7 @@ private byte[] json(Object object) {
}
}
- private ListenableFuture submit(
- Request request, ZendeskAsyncCompletionHandler handler) {
+ private ListenableFuture submit(Request request, AsyncCompletionHandler handler) {
if (logger.isDebugEnabled()) {
if (request.getStringData() != null) {
logger.debug(
@@ -3494,6 +3501,15 @@ private ListenableFuture submit(
return client.executeRequest(request, handler);
}
+ private ListenableFuture submit(
+ Request request,
+ AsyncCompletionHandler handler,
+ IdempotencyState idempotencyState) {
+ return submit(
+ IdempotencyUtil.addIdempotencyState(request, idempotencyState),
+ IdempotencyUtil.wrapHandler(handler, idempotencyState));
+ }
+
private abstract static class ZendeskAsyncCompletionHandler extends AsyncCompletionHandler {
@Override
public void onThrowable(Throwable t) {
@@ -3592,12 +3608,20 @@ public T onCompleted(Response response) throws Exception {
}
return mapper.convertValue(
mapper.readTree(response.getResponseBodyAsStream()).get(name), clazz);
- } else if (isRateLimitResponse(response)) {
+ }
+
+ if (isRateLimitResponse(response)) {
throw new ZendeskResponseRateLimitException(response);
}
+
+ if (isIdempotencyConflict(response)) {
+ throw new ZendeskResponseIdempotencyConflictException(response);
+ }
+
if (response.getStatusCode() == 404) {
return null;
}
+
throw new ZendeskResponseException(response);
}
}
@@ -3925,6 +3949,21 @@ private boolean isRateLimitResponse(Response response) {
return response.getStatusCode() == 429;
}
+ private boolean isIdempotencyConflict(Response response) throws IOException {
+ if (response.getStatusCode() != 400) {
+ return false;
+ }
+
+ try {
+ Map, ?> body = mapper.readValue(response.getResponseBody(), Map.class);
+ return IdempotencyUtil.IDEMPOTENCY_ERROR_NAME.equals(body.get("error"));
+ } catch (JsonProcessingException e) {
+ ZendeskResponseException exception = new ZendeskResponseException(response);
+ exception.addSuppressed(e);
+ throw exception;
+ }
+ }
+
//////////////////////////////////////////////////////////////////////
// Static helper methods
//////////////////////////////////////////////////////////////////////
@@ -3935,14 +3974,18 @@ private static T complete(ListenableFuture future) {
} catch (InterruptedException e) {
throw new ZendeskException(e.getMessage(), e);
} catch (ExecutionException e) {
+ if (e.getCause() instanceof ZendeskResponseRateLimitException) {
+ throw new ZendeskResponseRateLimitException(
+ (ZendeskResponseRateLimitException) e.getCause());
+ }
+ if (e.getCause() instanceof ZendeskResponseIdempotencyConflictException) {
+ throw new ZendeskResponseIdempotencyConflictException(
+ (ZendeskResponseIdempotencyConflictException) e.getCause());
+ }
+ if (e.getCause() instanceof ZendeskResponseException) {
+ throw new ZendeskResponseException((ZendeskResponseException) e.getCause());
+ }
if (e.getCause() instanceof ZendeskException) {
- if (e.getCause() instanceof ZendeskResponseRateLimitException) {
- throw new ZendeskResponseRateLimitException(
- (ZendeskResponseRateLimitException) e.getCause());
- }
- if (e.getCause() instanceof ZendeskResponseException) {
- throw new ZendeskResponseException((ZendeskResponseException) e.getCause());
- }
throw new ZendeskException(e.getCause());
}
throw new ZendeskException(e.getMessage(), e);
diff --git a/src/main/java/org/zendesk/client/v2/ZendeskResponseIdempotencyConflictException.java b/src/main/java/org/zendesk/client/v2/ZendeskResponseIdempotencyConflictException.java
new file mode 100644
index 00000000..978487f9
--- /dev/null
+++ b/src/main/java/org/zendesk/client/v2/ZendeskResponseIdempotencyConflictException.java
@@ -0,0 +1,38 @@
+package org.zendesk.client.v2;
+
+import java.io.IOException;
+import org.asynchttpclient.Response;
+
+/**
+ * Exception thrown when the Zendesk API returns an idempotency conflict error.
+ *
+ * This exception is thrown when a request is retried with the same idempotency key but
+ * different request parameters. The API returns a 400 status code with
+ * {@code error: "IdempotentRequestError"} to indicate that the request parameters don't match the
+ * original request associated with the idempotency key.
+ *
+ *
To resolve this error, either use a new idempotency key or ensure the request parameters
+ * match the original request.
+ *
+ * @see
+ * Zendesk API Idempotency
+ * @since 1.5.0
+ */
+public class ZendeskResponseIdempotencyConflictException extends ZendeskResponseException {
+
+ private static final long serialVersionUID = 1L;
+
+ public ZendeskResponseIdempotencyConflictException(Response res) throws IOException {
+ super(res);
+ }
+
+ public ZendeskResponseIdempotencyConflictException(
+ int statusCode, String statusText, String body) {
+ super(statusCode, statusText, body);
+ }
+
+ public ZendeskResponseIdempotencyConflictException(
+ ZendeskResponseIdempotencyConflictException cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/org/zendesk/client/v2/model/IdempotencyState.java b/src/main/java/org/zendesk/client/v2/model/IdempotencyState.java
new file mode 100644
index 00000000..ba651921
--- /dev/null
+++ b/src/main/java/org/zendesk/client/v2/model/IdempotencyState.java
@@ -0,0 +1,140 @@
+package org.zendesk.client.v2.model;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Represents the state of an idempotent operation in the Zendesk API.
+ *
+ *
This immutable class tracks the lifecycle of an idempotent request:
+ *
+ *
+ * - PENDING - Initial state before the request is sent
+ *
- CREATED - The resource was newly created (idempotency key miss)
+ *
- PREVIOUSLY_CREATED - The resource was previously created (idempotency key hit)
+ *
+ *
+ * @since 1.5.0
+ */
+public class IdempotencyState {
+
+ /**
+ * The status of an idempotent operation.
+ */
+ public enum Status {
+ /** Initial state, ready to be sent with a request. */
+ PENDING,
+ /** The resource was newly created (first request with this idempotency key). */
+ CREATED,
+ /** The resource was previously created (duplicate request with this idempotency key). */
+ PREVIOUSLY_CREATED
+ }
+
+ private final String idempotencyKey;
+ private final Status status;
+
+ /**
+ * Creates an IdempotencyState from an entity if it has an idempotency key.
+ *
+ * @param entity the entity to extract the idempotency key from
+ * @return an Optional containing a PENDING IdempotencyState if the entity has an idempotency
+ * key, or an empty Optional if the key is null
+ */
+ public static Optional of(IdempotentEntity entity) {
+ return Optional.ofNullable(entity.getIdempotencyKey())
+ .map(key -> new IdempotencyState(key, Status.PENDING));
+ }
+
+ /**
+ * Applies this state to the given entity, setting its idempotency fields.
+ *
+ * This method updates the entity's idempotency key and hit status based on the current state.
+ * The state must not be PENDING when calling this method.
+ *
+ * @param entity the entity to update, or null to skip the operation
+ * @throws IllegalStateException if this state is PENDING
+ * @throws IllegalArgumentException if the entity's idempotency key doesn't match this state's
+ * key
+ */
+ public void apply(IdempotentEntity entity) {
+ if (entity == null) {
+ return;
+ }
+
+ String entityKey = entity.getIdempotencyKey();
+ if (entityKey != null && !entityKey.equals(idempotencyKey)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Idempotency key mismatch: entity key = %s, state key = %s",
+ entityKey,
+ idempotencyKey));
+ }
+
+ if (status == Status.PENDING) {
+ throw new IllegalStateException(
+ String.format("Cannot apply idempotency state: %s", this));
+ }
+
+ entity.setIdempotencyKey(idempotencyKey);
+ entity.setIsIdempotencyHit(status == Status.PREVIOUSLY_CREATED);
+ }
+
+ public String getIdempotencyKey() {
+ return idempotencyKey;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "IdempotencyState{idempotencyKey=%s, status=%s}",
+ idempotencyKey,
+ status.name());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof IdempotencyState) {
+ IdempotencyState otherState = (IdempotencyState) other;
+ return Objects.equals(idempotencyKey, otherState.idempotencyKey)
+ && Objects.equals(status, otherState.status);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(idempotencyKey, status);
+ }
+
+ /**
+ * Creates a new state with status CREATED, indicating the resource was newly created.
+ *
+ * @return a new IdempotencyState with the same key and CREATED status
+ */
+ public IdempotencyState toCreated() {
+ return new IdempotencyState(idempotencyKey, Status.CREATED);
+ }
+
+ /**
+ * Creates a new state with status PREVIOUSLY_CREATED, indicating a duplicate request.
+ *
+ * @return a new IdempotencyState with the same key and PREVIOUSLY_CREATED status
+ */
+ public IdempotencyState toPreviouslyCreated() {
+ return new IdempotencyState(idempotencyKey, Status.PREVIOUSLY_CREATED);
+ }
+
+ private IdempotencyState(String idempotencyKey, Status status) {
+ this.idempotencyKey = idempotencyKey;
+ this.status = status;
+ }
+}
diff --git a/src/main/java/org/zendesk/client/v2/model/IdempotentEntity.java b/src/main/java/org/zendesk/client/v2/model/IdempotentEntity.java
new file mode 100644
index 00000000..483d7e1e
--- /dev/null
+++ b/src/main/java/org/zendesk/client/v2/model/IdempotentEntity.java
@@ -0,0 +1,70 @@
+package org.zendesk.client.v2.model;
+
+/**
+ * Interface for entities that support idempotent operations.
+ *
+ *
Entities implementing this interface can be created with an idempotency key to prevent
+ * duplicate resource creation. The Zendesk API uses idempotency keys to safely handle retries of
+ * create operations.
+ *
+ *
Usage example:
+ *
+ *
{@code
+ * Ticket ticket = new Ticket();
+ * ticket.setSubject("Help needed");
+ * ticket.setIdempotencyKey("unique-key-123"); // Prevents duplicate ticket creation
+ * Ticket created = zendesk.createTicket(ticket);
+ * if (Boolean.TRUE.equals(created.getIsIdempotencyHit())) {
+ * // This ticket was already created with this key
+ * }
+ * }
+ *
+ * @see
+ * Zendesk API Idempotency
+ * @since 1.5.0
+ */
+public interface IdempotentEntity {
+
+ /**
+ * Gets the idempotency key for this entity.
+ *
+ * @return the idempotency key, or null if not set
+ */
+ String getIdempotencyKey();
+
+ /**
+ * Sets the idempotency key for this entity.
+ *
+ * The idempotency key should be a unique string (e.g., a UUID) that identifies this specific
+ * create operation. If a request with the same key is retried, the API will return the
+ * previously created resource instead of creating a duplicate.
+ *
+ * @param idempotencyKey the idempotency key to use, or null to disable idempotency
+ */
+ void setIdempotencyKey(String idempotencyKey);
+
+ /**
+ * Indicates whether this entity was retrieved from a previous idempotent request.
+ *
+ *
After a successful create operation, this field will be:
+ *
+ *
+ * - {@code false} if the resource was newly created (idempotency key miss)
+ *
- {@code true} if the resource was previously created (idempotency key hit)
+ *
- {@code null} if idempotency was not used or the API didn't return the header
+ *
+ *
+ * @return true if this is a duplicate request, false if newly created, null if unknown
+ */
+ Boolean getIsIdempotencyHit();
+
+ /**
+ * Sets whether this entity was retrieved from a previous idempotent request.
+ *
+ * This is typically set automatically by the SDK based on response headers.
+ *
+ * @param isIdempotencyHit true if this is a duplicate request, false if newly created, null if
+ * unknown
+ */
+ void setIsIdempotencyHit(Boolean isIdempotencyHit);
+}
diff --git a/src/main/java/org/zendesk/client/v2/model/Ticket.java b/src/main/java/org/zendesk/client/v2/model/Ticket.java
index 341e96b3..04bae121 100644
--- a/src/main/java/org/zendesk/client/v2/model/Ticket.java
+++ b/src/main/java/org/zendesk/client/v2/model/Ticket.java
@@ -1,5 +1,6 @@
package org.zendesk.client.v2.model;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -12,7 +13,7 @@
* @since 04/04/2013 14:25
*/
@JsonIgnoreProperties(ignoreUnknown = true)
-public class Ticket extends Request implements SearchResultEntity {
+public class Ticket extends Request implements SearchResultEntity, IdempotentEntity {
private static final long serialVersionUID = -7559199410302237012L;
@@ -35,6 +36,8 @@ public class Ticket extends Request implements SearchResultEntity {
private Long brandId;
private Boolean isPublic;
private Boolean safeUpdate;
+ @JsonIgnore private String idempotencyKey;
+ @JsonIgnore private Boolean isIdempotencyHit;
public Ticket() {}
@@ -249,6 +252,28 @@ private Date getUpdatedStamp() {
return Boolean.TRUE.equals(safeUpdate) ? updatedAt : null;
}
+ @Override
+ @JsonIgnore
+ public String getIdempotencyKey() {
+ return idempotencyKey;
+ }
+
+ @Override
+ public void setIdempotencyKey(String idempotencyKey) {
+ this.idempotencyKey = idempotencyKey;
+ }
+
+ @Override
+ @JsonIgnore
+ public Boolean getIsIdempotencyHit() {
+ return isIdempotencyHit;
+ }
+
+ @Override
+ public void setIsIdempotencyHit(Boolean isIdempotencyHit) {
+ this.isIdempotencyHit = isIdempotencyHit;
+ }
+
@Override
public String toString() {
return "Ticket"
diff --git a/src/test/java/org/zendesk/client/v2/IdempotencyUtilTest.java b/src/test/java/org/zendesk/client/v2/IdempotencyUtilTest.java
new file mode 100644
index 00000000..1659d1d4
--- /dev/null
+++ b/src/test/java/org/zendesk/client/v2/IdempotencyUtilTest.java
@@ -0,0 +1,137 @@
+package org.zendesk.client.v2;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+import org.asynchttpclient.AsyncCompletionHandler;
+import org.asynchttpclient.Request;
+import org.asynchttpclient.RequestBuilder;
+import org.asynchttpclient.Response;
+import org.junit.Test;
+import org.zendesk.client.v2.model.IdempotencyState;
+import org.zendesk.client.v2.model.IdempotentEntity;
+
+public class IdempotencyUtilTest {
+
+ private static final Request REQUEST = new RequestBuilder("POST")
+ .setUrl("https://example.com")
+ .build();
+ private static final String KEY = "test-key-123";
+
+ @Test
+ public void addIdempotencyState_withNullState_returnsOriginalRequest() {
+ Request result = IdempotencyUtil.addIdempotencyState(REQUEST, null);
+
+ assertThat(result).isSameAs(REQUEST);
+ assertThat(result.getHeaders().get(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER)).isNull();
+ }
+
+ @Test
+ public void addIdempotencyState_withPendingState_addsIdempotencyKeyHeader() {
+ IdempotentEntity entity = createMockEntity();
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ Request result = IdempotencyUtil.addIdempotencyState(REQUEST, state);
+
+ assertThat(result).isNotSameAs(REQUEST);
+ assertThat(result.getHeaders().get(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER)).isEqualTo(KEY);
+ }
+
+ @Test
+ public void addIdempotencyState_withCreatedState_throwsIllegalArgumentException() {
+ IdempotentEntity entity = createMockEntity();
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow().toCreated();
+
+ assertThatThrownBy(() -> IdempotencyUtil.addIdempotencyState(REQUEST, state))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void addIdempotencyState_withPreviouslyCreatedState_throwsIllegalArgumentException() {
+ IdempotentEntity entity = createMockEntity();
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow().toPreviouslyCreated();
+
+ assertThatThrownBy(() -> IdempotencyUtil.addIdempotencyState(REQUEST, state))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void wrapHandler_withNullState_returnsOriginalHandler() {
+ @SuppressWarnings("unchecked")
+ AsyncCompletionHandler handler = mock(AsyncCompletionHandler.class);
+
+ AsyncCompletionHandler result =
+ IdempotencyUtil.wrapHandler(handler, null);
+
+ assertThat(result).isSameAs(handler);
+ }
+
+ @Test
+ public void wrapHandler_withMissHeader_setsIsIdempotencyHitToFalse() throws Exception {
+ testWrapHandler(false);
+ }
+
+ @Test
+ public void wrapHandler_withHitHeader_setsIsIdempotencyHitToTrue() throws Exception {
+ testWrapHandler(true);
+ }
+
+ @Test
+ public void wrapHandler_withMissingHeader_doesNotSetIdempotencyFields() throws Exception {
+ testWrapHandler(null);
+ }
+
+ @Test
+ public void wrapHandler_propagatesOnThrowable() {
+ IdempotentEntity entity = createMockEntity();
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ @SuppressWarnings("unchecked")
+ AsyncCompletionHandler originalHandler = mock(AsyncCompletionHandler.class);
+ Throwable throwable = new RuntimeException("test exception");
+
+ AsyncCompletionHandler wrappedHandler =
+ IdempotencyUtil.wrapHandler(originalHandler, state);
+ wrappedHandler.onThrowable(throwable);
+
+ verify(originalHandler).onThrowable(throwable);
+ }
+
+ private IdempotentEntity createMockEntity() {
+ IdempotentEntity entity = mock(IdempotentEntity.class);
+ when(entity.getIdempotencyKey()).thenReturn(KEY);
+ return entity;
+ }
+
+ private void testWrapHandler(Boolean isHit) throws Exception {
+ IdempotentEntity entity = createMockEntity();
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ @SuppressWarnings("unchecked")
+ AsyncCompletionHandler originalHandler = mock(AsyncCompletionHandler.class);
+ Response response = mock(Response.class);
+
+ String headerValue = Optional.ofNullable(isHit)
+ .map(hit -> hit
+ ? IdempotencyUtil.IDEMPOTENCY_LOOKUP_HIT
+ : IdempotencyUtil.IDEMPOTENCY_LOOKUP_MISS)
+ .orElse(null);
+ when(response.getHeader(IdempotencyUtil.IDEMPOTENCY_LOOKUP_HEADER)).thenReturn(headerValue);
+ when(originalHandler.onCompleted(response)).thenReturn(entity);
+
+ AsyncCompletionHandler wrappedHandler =
+ IdempotencyUtil.wrapHandler(originalHandler, state);
+
+ IdempotentEntity result = wrappedHandler.onCompleted(response);
+ assertThat(result).isSameAs(entity);
+
+ int numExpectedInvocations = isHit == null ? 0 : 1;
+ verify(entity, times(numExpectedInvocations)).setIdempotencyKey(KEY);
+ verify(entity, times(numExpectedInvocations)).setIsIdempotencyHit(isHit);
+ }
+}
diff --git a/src/test/java/org/zendesk/client/v2/TicketIdempotencyTest.java b/src/test/java/org/zendesk/client/v2/TicketIdempotencyTest.java
new file mode 100644
index 00000000..28404488
--- /dev/null
+++ b/src/test/java/org/zendesk/client/v2/TicketIdempotencyTest.java
@@ -0,0 +1,228 @@
+package org.zendesk.client.v2;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.absent;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
+import com.github.tomakehurst.wiremock.matching.StringValuePattern;
+import java.util.Collections;
+import java.util.function.Function;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.zendesk.client.v2.model.Comment;
+import org.zendesk.client.v2.model.Status;
+import org.zendesk.client.v2.model.Ticket;
+
+/**
+ * Integration tests for ticket creation with idempotency key support.
+ * Uses WireMock to simulate Zendesk API responses.
+ */
+public class TicketIdempotencyTest {
+
+ private static final String CREATE_TICKET_PATH = "/api/v2/tickets.json";
+ private static final long TICKET_ID = 12345L;
+ private static final String TICKET_KEY = "test-key-123";
+
+ @ClassRule
+ public static WireMockClassRule zendeskApiClass =
+ new WireMockClassRule(options().dynamicPort().dynamicHttpsPort());
+
+ @Rule public WireMockClassRule zendeskApiMock = zendeskApiClass;
+
+ private Zendesk client;
+ private final ObjectMapper objectMapper = Zendesk.createMapper(Function.identity());
+
+ @Before
+ public void setUp() {
+ client = new Zendesk.Builder("http://localhost:" + zendeskApiMock.port())
+ .setUsername("zana@example.com")
+ .setToken("still-sane-exile")
+ .build();
+ }
+
+ @After
+ public void tearDown() {
+ client.close();
+ client = null;
+ }
+
+ @Test
+ public void createTicket_withoutIdempotencyKey_doesNotSendHeader() throws JsonProcessingException {
+ Ticket requestTicket = createSampleTicket();
+ requestTicket.setIdempotencyKey(null);
+
+ Ticket responseTicket = createResponseTicket();
+ String expectedJsonResponse =
+ objectMapper.writeValueAsString(Collections.singletonMap("ticket", responseTicket));
+
+ zendeskApiMock.stubFor(
+ post(urlEqualTo(CREATE_TICKET_PATH))
+ .willReturn(ok().withBody(expectedJsonResponse)));
+
+ Ticket result = client.createTicket(requestTicket);
+
+ verifyRequest(null);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo(TICKET_ID);
+ assertThat(result.getIdempotencyKey()).isNull();
+ assertThat(result.getIsIdempotencyHit()).isNull();
+ }
+
+ @Test
+ public void createTicket_withIdempotencyKeyFirstRequest_sendsMissHeader()
+ throws JsonProcessingException {
+ Ticket requestTicket = createSampleTicket();
+ requestTicket.setIdempotencyKey(TICKET_KEY);
+
+ Ticket responseTicket = createResponseTicket();
+ String expectedJsonResponse =
+ objectMapper.writeValueAsString(Collections.singletonMap("ticket", responseTicket));
+
+ zendeskApiMock.stubFor(
+ post(urlEqualTo(CREATE_TICKET_PATH))
+ .withHeader(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER, equalTo(TICKET_KEY))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader(
+ IdempotencyUtil.IDEMPOTENCY_LOOKUP_HEADER,
+ IdempotencyUtil.IDEMPOTENCY_LOOKUP_MISS)
+ .withBody(expectedJsonResponse)));
+
+ Ticket result = client.createTicket(requestTicket);
+
+ verifyRequest(TICKET_KEY);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo(TICKET_ID);
+ assertThat(result.getIdempotencyKey()).isEqualTo(TICKET_KEY);
+ assertThat(result.getIsIdempotencyHit()).isFalse();
+ }
+
+ @Test
+ public void createTicket_withIdempotencyKeyDuplicateRequest_sendsHitHeader()
+ throws JsonProcessingException {
+ Ticket requestTicket = createSampleTicket();
+ requestTicket.setIdempotencyKey(TICKET_KEY);
+
+ Ticket responseTicket = createResponseTicket();
+ String expectedJsonResponse =
+ objectMapper.writeValueAsString(Collections.singletonMap("ticket", responseTicket));
+
+ zendeskApiMock.stubFor(
+ post(urlEqualTo(CREATE_TICKET_PATH))
+ .withHeader(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER, equalTo(TICKET_KEY))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withHeader(
+ IdempotencyUtil.IDEMPOTENCY_LOOKUP_HEADER,
+ IdempotencyUtil.IDEMPOTENCY_LOOKUP_HIT)
+ .withBody(expectedJsonResponse)));
+
+ Ticket result = client.createTicket(requestTicket);
+
+ verifyRequest(TICKET_KEY);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo(TICKET_ID);
+ assertThat(result.getIdempotencyKey()).isEqualTo(TICKET_KEY);
+ assertThat(result.getIsIdempotencyHit()).isTrue();
+ }
+
+ @Test
+ public void createTicket_onIdempotencyConflictError_throwsException() {
+ Ticket requestTicket = createSampleTicket();
+ requestTicket.setIdempotencyKey(TICKET_KEY);
+
+ JsonNode expectedJsonResponse = JsonNodeFactory.instance.objectNode()
+ .put("error", IdempotencyUtil.IDEMPOTENCY_ERROR_NAME)
+ .put("description", "Request parameters don't match the given idempotency key");
+
+ zendeskApiMock.stubFor(
+ post(urlEqualTo(CREATE_TICKET_PATH))
+ .withHeader(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER, equalTo(TICKET_KEY))
+ .willReturn(
+ aResponse()
+ .withStatus(400)
+ .withJsonBody(expectedJsonResponse)));
+
+ assertThatThrownBy(() -> client.createTicket(requestTicket)).isInstanceOf(
+ ZendeskResponseIdempotencyConflictException.class);
+
+ verifyRequest(TICKET_KEY);
+ }
+
+ @Test
+ public void createTicket_withIdempotencyKeyNoHeader_doesNotSetIdempotencyFields()
+ throws JsonProcessingException {
+ Ticket requestTicket = createSampleTicket();
+ requestTicket.setIdempotencyKey(TICKET_KEY);
+
+ Ticket responseTicket = createResponseTicket();
+ String expectedJsonResponse =
+ objectMapper.writeValueAsString(Collections.singletonMap("ticket", responseTicket));
+
+ zendeskApiMock.stubFor(
+ post(urlEqualTo(CREATE_TICKET_PATH))
+ .withHeader(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER, equalTo(TICKET_KEY))
+ .willReturn(
+ aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody(expectedJsonResponse)));
+
+ Ticket result = client.createTicket(requestTicket);
+
+ zendeskApiMock.verify(
+ postRequestedFor(urlEqualTo(CREATE_TICKET_PATH))
+ .withHeader(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER, equalTo(TICKET_KEY)));
+
+ assertThat(result).isNotNull();
+ assertThat(result.getId()).isEqualTo(TICKET_ID);
+ // Idempotency fields should not be set if server doesn't return the header
+ assertThat(result.getIdempotencyKey()).isNull();
+ assertThat(result.getIsIdempotencyHit()).isNull();
+ }
+
+ private Ticket createSampleTicket() {
+ Ticket ticket = new Ticket();
+ ticket.setSubject("Test Ticket");
+ ticket.setComment(new Comment("This is a test ticket"));
+ ticket.setRequesterId(123456L);
+ return ticket;
+ }
+
+ private Ticket createResponseTicket() {
+ Ticket ticket = createSampleTicket();
+ ticket.setId(TICKET_ID);
+ ticket.setStatus(Status.OPEN);
+ return ticket;
+ }
+
+ private void verifyRequest(String idempotencyKey) {
+ StringValuePattern pattern = idempotencyKey == null
+ ? absent()
+ : equalTo(idempotencyKey);
+ zendeskApiMock.verify(
+ postRequestedFor(urlEqualTo(CREATE_TICKET_PATH))
+ .withHeader(IdempotencyUtil.IDEMPOTENCY_KEY_HEADER, pattern));
+ }
+}
diff --git a/src/test/java/org/zendesk/client/v2/model/IdempotencyStateTest.java b/src/test/java/org/zendesk/client/v2/model/IdempotencyStateTest.java
new file mode 100644
index 00000000..2c260e01
--- /dev/null
+++ b/src/test/java/org/zendesk/client/v2/model/IdempotencyStateTest.java
@@ -0,0 +1,201 @@
+package org.zendesk.client.v2.model;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+import org.junit.Test;
+import org.zendesk.client.v2.model.IdempotencyState.Status;
+
+public class IdempotencyStateTest {
+
+ private static final String KEY = "test-key-123";
+ private static final String OTHER_KEY = "test-key-456";
+
+ @Test
+ public void of_withIdempotencyKey_createsPendingState() {
+ IdempotentEntity entity = createMockEntity(KEY);
+
+ Optional result = IdempotencyState.of(entity);
+
+ assertThat(result).isPresent();
+ IdempotencyState state = result.get();
+ assertThat(state.getIdempotencyKey()).isEqualTo(KEY);
+ assertThat(state.getStatus()).isEqualTo(Status.PENDING);
+ }
+
+ @Test
+ public void of_withNullIdempotencyKey_returnsEmptyOptional() {
+ IdempotentEntity entity = createMockEntity(null);
+
+ Optional result = IdempotencyState.of(entity);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void apply_withCreatedState_setsEntityFieldsCorrectly() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow().toCreated();
+
+ state.apply(entity);
+
+ verify(entity).setIdempotencyKey(KEY);
+ verify(entity).setIsIdempotencyHit(false);
+ }
+
+ @Test
+ public void apply_withPreviouslyCreatedState_setsEntityFieldsCorrectly() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow().toPreviouslyCreated();
+
+ state.apply(entity);
+
+ verify(entity).setIdempotencyKey(KEY);
+ verify(entity).setIsIdempotencyHit(true);
+ }
+
+ @Test
+ public void apply_withPendingState_throwsIllegalStateException() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ assertThatThrownBy(() -> state.apply(entity)).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void apply_withMismatchedKey_throwsIllegalArgumentException() {
+ assertThat(OTHER_KEY).isNotEqualTo(KEY);
+ IdempotentEntity entity = createMockEntity(OTHER_KEY);
+ IdempotencyState state = IdempotencyState.of(createMockEntity(KEY)).orElseThrow().toCreated();
+
+ assertThatThrownBy(() -> state.apply(entity)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void apply_withNullEntity_doesNotThrow() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow().toCreated();
+
+ assertThatNoException().isThrownBy(() -> state.apply(null));
+ }
+
+ @Test
+ public void toCreated_transitionsCorrectly() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState pendingState = IdempotencyState.of(entity).orElseThrow();
+
+ IdempotencyState createdState = pendingState.toCreated();
+
+ assertThat(createdState.getIdempotencyKey()).isEqualTo(KEY);
+ assertThat(createdState.getStatus()).isEqualTo(Status.CREATED);
+ // Original state should not change
+ assertThat(pendingState.getStatus()).isEqualTo(Status.PENDING);
+ }
+
+ @Test
+ public void toPreviouslyCreated_transitionsCorrectly() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState pendingState = IdempotencyState.of(entity).orElseThrow();
+
+ IdempotencyState previouslyCreatedState = pendingState.toPreviouslyCreated();
+
+ assertThat(previouslyCreatedState.getIdempotencyKey()).isEqualTo(KEY);
+ assertThat(previouslyCreatedState.getStatus()).isEqualTo(Status.PREVIOUSLY_CREATED);
+ // Original state should not change
+ assertThat(pendingState.getStatus()).isEqualTo(Status.PENDING);
+ }
+
+ @Test
+ public void equals_withSameState_returnsTrue() {
+ IdempotentEntity entity1 = createMockEntity(KEY);
+ IdempotentEntity entity2 = createMockEntity(KEY);
+
+ IdempotencyState state1 = IdempotencyState.of(entity1).orElseThrow();
+ IdempotencyState state2 = IdempotencyState.of(entity2).orElseThrow();
+
+ assertThat(state1).isEqualTo(state2);
+ }
+
+ @Test
+ public void equals_withDifferentKey_returnsFalse() {
+ IdempotentEntity entity1 = createMockEntity(KEY);
+ IdempotentEntity entity2 = createMockEntity(OTHER_KEY);
+
+ IdempotencyState state1 = IdempotencyState.of(entity1).orElseThrow();
+ IdempotencyState state2 = IdempotencyState.of(entity2).orElseThrow();
+
+ assertThat(state1).isNotEqualTo(state2);
+ }
+
+ @Test
+ public void equals_withDifferentStatus_returnsFalse() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState pendingState = IdempotencyState.of(entity).orElseThrow();
+
+ assertThat(pendingState)
+ .isNotEqualTo(pendingState.toCreated())
+ .isNotEqualTo(pendingState.toPreviouslyCreated());
+ assertThat(pendingState.toCreated()).isNotEqualTo(pendingState.toPreviouslyCreated());
+ }
+
+ @Test
+ public void equals_withItself_returnsTrue() {
+ IdempotentEntity entity = createMockEntity(KEY);
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ assertThat(state).isEqualTo(state);
+ }
+
+ @Test
+ public void equals_withNull_returnsFalse() {
+ IdempotentEntity entity = createMockEntity("test-key-123");
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ assertThat(state).isNotEqualTo(null);
+ }
+
+ @Test
+ public void equals_withDifferentType_returnsFalse() {
+ IdempotentEntity entity = createMockEntity("test-key-123");
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ assertThat(state).isNotEqualTo("not an IdempotencyState");
+ }
+
+ @Test
+ public void hashCode_withSameState_returnsSameHashCode() {
+ IdempotentEntity entity1 = createMockEntity(KEY);
+ IdempotentEntity entity2 = createMockEntity(KEY);
+
+ IdempotencyState state1 = IdempotencyState.of(entity1).orElseThrow();
+ IdempotencyState state2 = IdempotencyState.of(entity2).orElseThrow();
+
+ assertThat(state1).hasSameHashCodeAs(state2);
+ assertThat(state1.toCreated()).hasSameHashCodeAs(state2.toCreated());
+ assertThat(state1.toPreviouslyCreated()).hasSameHashCodeAs(state2.toPreviouslyCreated());
+ }
+
+ @Test
+ public void toString_returnsFormattedString() {
+ IdempotentEntity entity = createMockEntity("test-key-123");
+ IdempotencyState state = IdempotencyState.of(entity).orElseThrow();
+
+ String result = state.toString();
+
+ assertThat(result)
+ .contains("IdempotencyState")
+ .contains("test-key-123")
+ .contains("PENDING");
+ }
+
+ private IdempotentEntity createMockEntity(String idempotencyKey) {
+ IdempotentEntity entity = mock(IdempotentEntity.class);
+ when(entity.getIdempotencyKey()).thenReturn(idempotencyKey);
+ return entity;
+ }
+}
diff --git a/src/test/java/org/zendesk/client/v2/model/TicketTest.java b/src/test/java/org/zendesk/client/v2/model/TicketTest.java
index c8639379..d06f48bb 100644
--- a/src/test/java/org/zendesk/client/v2/model/TicketTest.java
+++ b/src/test/java/org/zendesk/client/v2/model/TicketTest.java
@@ -3,54 +3,109 @@
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.databind.ObjectMapper;
-import java.util.Calendar;
+import com.fasterxml.jackson.databind.util.StdDateFormat;
import java.util.Date;
-import java.util.Random;
+import java.util.Map;
import java.util.function.Function;
import org.junit.Test;
import org.zendesk.client.v2.Zendesk;
public class TicketTest {
- private static final Random RANDOM = new Random();
- private static final String TICKET_COMMENT1 = "Please ignore this ticket";
- private static final Date NOW = Calendar.getInstance().getTime();
+ private static final long TICKET_ID = 12345;
+ private static final String TICKET_SUBJECT = "Test subject";
+ private static final Status TICKET_STATUS = Status.OPEN;
+ private static final Date TICKET_TS = new Date();
+
+ private static final Map TICKET_JSON_MAP = Map.of(
+ "id", TICKET_ID,
+ "subject", TICKET_SUBJECT,
+ "status", TICKET_STATUS.toString(),
+ "updated_at", new StdDateFormat().format(TICKET_TS),
+ "has_incidents", false);
+
+ private static final String TICKET_IDEMPOTENCY_KEY = "test-key-123";
+
+ private static final ObjectMapper OBJECT_MAPPER = Zendesk.createMapper(Function.identity());
@Test
public void serializeWithNullSafeUpdate() throws Exception {
- ObjectMapper mapper = Zendesk.createMapper(Function.identity());
Ticket ticket = createSampleTicket();
- assertThat(mapper.writeValueAsString(ticket))
+ assertThat(OBJECT_MAPPER.writeValueAsString(ticket))
.doesNotContain("\"safe_update\"")
.doesNotContain("\"updated_stamp\"");
}
@Test
public void serializeWithFalseSafeUpdate() throws Exception {
- ObjectMapper mapper = Zendesk.createMapper(Function.identity());
Ticket ticket = createSampleTicket();
ticket.setSafeUpdate(false);
- assertThat(mapper.writeValueAsString(ticket))
+ assertThat(OBJECT_MAPPER.writeValueAsString(ticket))
.doesNotContain("\"safe_update\"")
.doesNotContain("\"updated_stamp\"");
}
@Test
public void serializeWithSafeUpdate() throws Exception {
- ObjectMapper mapper = Zendesk.createMapper(Function.identity());
Ticket ticket = createSampleTicket();
ticket.setSafeUpdate(true);
- assertThat(mapper.writeValueAsString(ticket))
+ assertThat(OBJECT_MAPPER.writeValueAsString(ticket))
.contains("\"safe_update\"")
.contains("\"updated_stamp\"");
}
+ @Test
+ public void idempotencyFields_areNotSerialized() throws Exception {
+ Ticket ticket = createSampleTicket();
+ ticket.setIdempotencyKey("test-idempotency-key");
+ ticket.setIsIdempotencyHit(true);
+
+ String json = OBJECT_MAPPER.writeValueAsString(ticket);
+ Map, ?> jsonMap = OBJECT_MAPPER.readValue(json, Map.class);
+ assertThat(jsonMap).isEqualTo(TICKET_JSON_MAP);
+ }
+
+ @Test
+ public void ticket_canBeDeserializedWithoutIdempotencyFields() throws Exception {
+ String json = OBJECT_MAPPER.writeValueAsString(TICKET_JSON_MAP);
+ Ticket ticket = OBJECT_MAPPER.readValue(json, Ticket.class);
+
+ assertThat(ticket).isNotNull();
+ assertThat(ticket.getId()).isEqualTo(TICKET_ID);
+ assertThat(ticket.getSubject()).isEqualTo(TICKET_SUBJECT);
+ assertThat(ticket.getStatus()).isEqualTo(TICKET_STATUS);
+ assertThat(ticket.getIdempotencyKey()).isNull();
+ assertThat(ticket.getIsIdempotencyHit()).isNull();
+ }
+
+ @Test
+ public void idempotencyKey_getterSetterWork() {
+ Ticket ticket = createSampleTicket();
+ ticket.setIdempotencyKey(TICKET_IDEMPOTENCY_KEY);
+
+ assertThat(ticket.getIdempotencyKey()).isEqualTo(TICKET_IDEMPOTENCY_KEY);
+ }
+
+ @Test
+ public void isIdempotencyHit_getterSetterWork() {
+ Ticket ticket = createSampleTicket();
+
+ ticket.setIsIdempotencyHit(true);
+ assertThat(ticket.getIsIdempotencyHit()).isTrue();
+
+ ticket.setIsIdempotencyHit(false);
+ assertThat(ticket.getIsIdempotencyHit()).isFalse();
+
+ ticket.setIsIdempotencyHit(null);
+ assertThat(ticket.getIsIdempotencyHit()).isNull();
+ }
+
private Ticket createSampleTicket() {
Ticket ticket = new Ticket();
- ticket.setId(Math.abs(RANDOM.nextLong()));
- ticket.setComment(new Comment(TICKET_COMMENT1));
- ticket.setUpdatedAt(NOW);
- ticket.setCustomStatusId(Math.abs(RANDOM.nextLong()));
+ ticket.setId(TICKET_ID);
+ ticket.setSubject(TICKET_SUBJECT);
+ ticket.setStatus(TICKET_STATUS);
+ ticket.setUpdatedAt(TICKET_TS);
return ticket;
}
}