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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions examples/java/io/mailtrap/examples/general/ApiTokensExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import io.mailtrap.model.ResourceType;
import io.mailtrap.model.request.apitokens.ApiTokenResource;
import io.mailtrap.model.request.apitokens.CreateApiTokenRequest;
import io.mailtrap.model.request.apitokens.ResetApiTokenRequest;
import io.mailtrap.model.request.apitokens.TokenExpiration;

import java.time.OffsetDateTime;
import java.util.List;

public class ApiTokensExample {
Expand All @@ -22,8 +25,13 @@ public static void main(String[] args) {
final var client = MailtrapClientFactory.createMailtrapClient(config);

// The full token value is returned only on creation — store it securely.
// Expiration is optional: omit it for the server default (a 1-year default is being
// rolled out), pass TokenExpiration.never() for a token that never expires, or pass
// TokenExpiration.at(...) for a concrete expiration (must be in the future and no
// more than 5 years ahead, otherwise the API responds with a 422 error).
final var createRequest = new CreateApiTokenRequest(
"My token",
TokenExpiration.at(OffsetDateTime.now().plusMonths(6)),
List.of(new ApiTokenResource(ResourceType.ACCOUNT, ACCOUNT_ID, AccessLevel.VIEWER)));

final var createdToken = client.generalApi().apiTokens()
Expand All @@ -39,8 +47,11 @@ public static void main(String[] args) {
System.out.println(token);

// Reset expires the existing token and returns a new one with the same permissions.
// The new token value is only returned here.
final var resetToken = client.generalApi().apiTokens().resetApiToken(ACCOUNT_ID, tokenId);
// The new token value is only returned here. Without a request body the new token
// gets the server default expiration; the overload with ResetApiTokenRequest sets it
// explicitly (here: a token that never expires).
final var resetToken = client.generalApi().apiTokens()
.resetApiToken(ACCOUNT_ID, tokenId, new ResetApiTokenRequest(TokenExpiration.never()));
System.out.println(resetToken);

client.generalApi().apiTokens().deleteApiToken(ACCOUNT_ID, resetToken.getId());
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/mailtrap/api/apitokens/ApiTokens.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.mailtrap.api.apitokens;

import io.mailtrap.model.request.apitokens.CreateApiTokenRequest;
import io.mailtrap.model.request.apitokens.ResetApiTokenRequest;
import io.mailtrap.model.response.apitokens.ApiToken;
import io.mailtrap.model.response.apitokens.ApiTokenWithToken;

Expand Down Expand Up @@ -53,4 +54,16 @@ public interface ApiTokens {
*/
ApiTokenWithToken resetApiToken(long accountId, long id);

/**
* Reset an API token. Expires the requested token and creates a new one with the same
* permissions; the new token value is returned only once. The request can set the new
* token expiration; omit it for the server default.
*
* @param accountId unique account ID
* @param id API token ID
* @param request optional new token expiration
* @return new token, including the full token value
*/
ApiTokenWithToken resetApiToken(long accountId, long id, ResetApiTokenRequest request);

}
11 changes: 11 additions & 0 deletions src/main/java/io/mailtrap/api/apitokens/ApiTokensImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.mailtrap.http.RequestData;
import io.mailtrap.model.AbstractModel;
import io.mailtrap.model.request.apitokens.CreateApiTokenRequest;
import io.mailtrap.model.request.apitokens.ResetApiTokenRequest;
import io.mailtrap.model.response.apitokens.ApiToken;
import io.mailtrap.model.response.apitokens.ApiTokenWithToken;

Expand Down Expand Up @@ -64,4 +65,14 @@ public ApiTokenWithToken resetApiToken(final long accountId, final long id) {
ApiTokenWithToken.class
);
}

@Override
public ApiTokenWithToken resetApiToken(final long accountId, final long id, final ResetApiTokenRequest request) {
return httpClient.post(
String.format(apiHost + "/api/accounts/%d/api_tokens/%d/reset", accountId, id),
request,
new RequestData(),
ApiTokenWithToken.class
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.mailtrap.model.request.apitokens;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.mailtrap.model.AbstractModel;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -16,6 +18,19 @@ public class CreateApiTokenRequest extends AbstractModel {

private String name;

/**
* Optional token expiration as an ISO 8601 date-time. Omit (or leave null) for the server
* default (a 1-year default is being rolled out). Use {@link TokenExpiration#never()} for
* a token that never expires. Past or more-than-5-years-ahead values are rejected with 422.
*/
@JsonProperty("expires_at")
@JsonInclude(JsonInclude.Include.NON_NULL)
private TokenExpiration expiresAt;

private List<ApiTokenResource> resources;

public CreateApiTokenRequest(final String name, final List<ApiTokenResource> resources) {
this(name, null, resources);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.mailtrap.model.request.apitokens;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.mailtrap.model.AbstractModel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ResetApiTokenRequest extends AbstractModel {

/**
* Optional token expiration as an ISO 8601 date-time. Omit (or leave null) for the server
* default (a 1-year default is being rolled out). Use {@link TokenExpiration#never()} for
* a token that never expires. Past or more-than-5-years-ahead values are rejected with 422.
*/
@JsonProperty("expires_at")
@JsonInclude(JsonInclude.Include.NON_NULL)
private TokenExpiration expiresAt;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.mailtrap.model.request.apitokens;

import com.fasterxml.jackson.annotation.JsonValue;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;

/**
* API token expiration sent as the {@code expires_at} request field.
* Use {@link #at(OffsetDateTime)} for a concrete expiration or {@link #never()} for a token
* that never expires (serialized as JSON {@code null}).
*/
public final class TokenExpiration {

private final String value;

private TokenExpiration(final String value) {
this.value = value;
}

/**
* Token expires at the given moment. Past or more-than-5-years-ahead values are rejected
* by the API with a 422 error.
*
* @param value expiration date-time
* @return expiration serialized as an ISO 8601 date-time string
*/
public static TokenExpiration at(final OffsetDateTime value) {
return new TokenExpiration(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value));
}

/**
* Token never expires.
*
* @return expiration serialized as JSON {@code null}
*/
public static TokenExpiration never() {
return new TokenExpiration(null);
}

@JsonValue
public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public class ApiTokenSpecifier extends Specifier {

private String token;

@JsonProperty("masked_token")
private String maskedToken;

@JsonProperty("expires_at")
private OffsetDateTime expiresAt;

Expand Down
133 changes: 131 additions & 2 deletions src/test/java/io/mailtrap/api/apitokens/ApiTokensImplTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package io.mailtrap.api.apitokens;

import com.fasterxml.jackson.databind.JsonNode;
import io.mailtrap.Constants;
import io.mailtrap.Mapper;
import io.mailtrap.config.MailtrapConfig;
import io.mailtrap.exception.http.HttpClientException;
import io.mailtrap.factory.MailtrapClientFactory;
import io.mailtrap.http.CustomHttpClient;
import io.mailtrap.http.RequestData;
import io.mailtrap.model.AccessLevel;
import io.mailtrap.model.ResourceType;
import io.mailtrap.model.request.apitokens.ApiTokenResource;
import io.mailtrap.model.request.apitokens.CreateApiTokenRequest;
import io.mailtrap.model.request.apitokens.ResetApiTokenRequest;
import io.mailtrap.model.request.apitokens.TokenExpiration;
import io.mailtrap.model.response.apitokens.ApiToken;
import io.mailtrap.model.response.apitokens.ApiTokenWithToken;
import io.mailtrap.testutils.BaseTest;
Expand All @@ -15,15 +22,26 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class ApiTokensImplTest extends BaseTest {

private final long apiTokenId = 12345L;
private final long resetWithBodyApiTokenId = 54321L;

private ApiTokens api;

Expand All @@ -36,14 +54,26 @@ public void init() {
DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens",
"POST", "api/apitokens/createApiTokenRequest.json", "api/apitokens/createApiTokenResponse.json"),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens",
"POST", "api/apitokens/createApiTokenNeverExpiresRequest.json", "api/apitokens/createApiTokenNeverExpiresResponse.json"),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens",
"POST", "api/apitokens/createApiTokenWithExpirationRequest.json", "api/apitokens/createApiTokenWithExpirationResponse.json"),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens/" + apiTokenId,
"GET", null, "api/apitokens/getApiTokenResponse.json"),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens/" + apiTokenId,
"DELETE", null, null),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens/" + apiTokenId + "/reset",
"POST", null, "api/apitokens/resetApiTokenResponse.json")
"POST", null, "api/apitokens/resetApiTokenResponse.json"),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens/" + resetWithBodyApiTokenId + "/reset",
"POST", "api/apitokens/resetApiTokenNeverExpiresRequest.json", "api/apitokens/resetApiTokenNeverExpiresResponse.json"),

DataMock.build(Constants.GENERAL_HOST + "/api/accounts/" + accountId + "/api_tokens/" + resetWithBodyApiTokenId + "/reset",
"POST", "api/apitokens/resetApiTokenWithExpirationRequest.json", "api/apitokens/resetApiTokenWithExpirationResponse.json")
));

final MailtrapConfig testConfig = new MailtrapConfig.Builder()
Expand All @@ -69,12 +99,15 @@ void test_getAllApiTokens() {
}

@Test
void test_createApiToken() {
void test_createApiToken() throws IOException {
final CreateApiTokenRequest request = new CreateApiTokenRequest(
"Scratch test token",
List.of(new ApiTokenResource(ResourceType.ACCOUNT, accountId, AccessLevel.ADMIN))
);

final JsonNode body = Mapper.get().readTree(request.toJson());
assertFalse(body.has("expires_at"));

final ApiTokenWithToken response = api.createApiToken(accountId, request);

assertNotNull(response);
Expand All @@ -84,6 +117,69 @@ void test_createApiToken() {
assertNull(response.getExpiresAt());
}

@Test
void test_createApiToken_neverExpires() throws IOException {
final CreateApiTokenRequest request = new CreateApiTokenRequest(
"Never expiring token",
TokenExpiration.never(),
List.of(new ApiTokenResource(ResourceType.ACCOUNT, accountId, AccessLevel.ADMIN))
);

final JsonNode body = Mapper.get().readTree(request.toJson());
assertTrue(body.has("expires_at"));
assertTrue(body.get("expires_at").isNull());

final ApiTokenWithToken response = api.createApiToken(accountId, request);

assertNotNull(response);
assertEquals(23456L, response.getId());
assertEquals("neverexpires123", response.getToken());
assertNull(response.getExpiresAt());
}

@Test
void test_createApiToken_withExpiration() throws IOException {
final CreateApiTokenRequest request = new CreateApiTokenRequest(
"Expiring token",
TokenExpiration.at(OffsetDateTime.parse("2027-06-01T00:00:00Z")),
List.of(new ApiTokenResource(ResourceType.ACCOUNT, accountId, AccessLevel.ADMIN))
);

final JsonNode body = Mapper.get().readTree(request.toJson());
assertEquals("2027-06-01T00:00:00Z", body.get("expires_at").asText());

final ApiTokenWithToken response = api.createApiToken(accountId, request);

assertNotNull(response);
assertEquals(34567L, response.getId());
assertEquals("expiring123", response.getToken());
assertEquals(OffsetDateTime.parse("2027-06-01T00:00:00Z"), response.getExpiresAt());
}

@Test
void test_createApiToken_invalidExpiration_throwsHttpClientException() {
final CustomHttpClient failingHttpClient = mock(CustomHttpClient.class);
when(failingHttpClient.post(anyString(), any(CreateApiTokenRequest.class), any(RequestData.class), eq(ApiTokenWithToken.class)))
.thenThrow(new HttpClientException("Expires at must be no more than 5 years in the future", 422));

final MailtrapConfig failingConfig = new MailtrapConfig.Builder()
.httpClient(failingHttpClient)
.token("dummy_token")
.build();

final ApiTokens failingApi = MailtrapClientFactory.createMailtrapClient(failingConfig).generalApi().apiTokens();

final CreateApiTokenRequest request = new CreateApiTokenRequest(
"Token with invalid expiration",
TokenExpiration.at(OffsetDateTime.parse("2050-01-01T00:00:00Z")),
List.of(new ApiTokenResource(ResourceType.ACCOUNT, accountId, AccessLevel.ADMIN))
);

final HttpClientException exception = assertThrows(HttpClientException.class,
() -> failingApi.createApiToken(accountId, request));
assertEquals(422, exception.getStatusCode());
}

@Test
void test_getApiToken() {
final ApiToken token = api.getApiToken(accountId, apiTokenId);
Expand All @@ -108,4 +204,37 @@ void test_resetApiToken() {
assertEquals("newtoken123", response.getToken());
assertEquals("n3w0", response.getLast4Digits());
}

@Test
void test_resetApiToken_neverExpires() throws IOException {
final ResetApiTokenRequest request = new ResetApiTokenRequest(TokenExpiration.never());

final JsonNode body = Mapper.get().readTree(request.toJson());
assertTrue(body.has("expires_at"));
assertTrue(body.get("expires_at").isNull());

final ApiTokenWithToken response = api.resetApiToken(accountId, resetWithBodyApiTokenId, request);

assertNotNull(response);
assertEquals(resetWithBodyApiTokenId, response.getId());
assertEquals("resetnever123", response.getToken());
assertNull(response.getExpiresAt());
}

@Test
void test_resetApiToken_withExpiration() throws IOException {
final ResetApiTokenRequest request = new ResetApiTokenRequest(
TokenExpiration.at(OffsetDateTime.parse("2027-06-01T00:00:00Z"))
);

final JsonNode body = Mapper.get().readTree(request.toJson());
assertEquals("2027-06-01T00:00:00Z", body.get("expires_at").asText());

final ApiTokenWithToken response = api.resetApiToken(accountId, resetWithBodyApiTokenId, request);

assertNotNull(response);
assertEquals(resetWithBodyApiTokenId, response.getId());
assertEquals("resetexpiring123", response.getToken());
assertEquals(OffsetDateTime.parse("2027-06-01T00:00:00Z"), response.getExpiresAt());
}
}
Loading
Loading