You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by ad...@apache.org on 2018/04/06 13:20:29 UTC
[10/24] james-project git commit: JAMES-2366 Introduce REST API for
forward
JAMES-2366 Introduce REST API for forward
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/92f4a682
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/92f4a682
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/92f4a682
Branch: refs/heads/master
Commit: 92f4a6823a22164341135eb08603625f02449dc3
Parents: 53d760b
Author: benwa <bt...@linagora.com>
Authored: Thu Mar 29 16:16:07 2018 +0700
Committer: Antoine Duprat <ad...@linagora.com>
Committed: Fri Apr 6 15:04:48 2018 +0200
----------------------------------------------------------------------
.../dto/ForwardDestinationResponse.java | 32 +
.../james/webadmin/routes/ForwardRoutes.java | 263 ++++++++
.../webadmin/routes/ForwardRoutesTest.java | 676 +++++++++++++++++++
3 files changed, 971 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/james-project/blob/92f4a682/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/ForwardDestinationResponse.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/ForwardDestinationResponse.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/ForwardDestinationResponse.java
new file mode 100644
index 0000000..5e681c0
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/dto/ForwardDestinationResponse.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.dto;
+
+public class ForwardDestinationResponse {
+ private final String mailAddress;
+
+ public ForwardDestinationResponse(String mailAddress) {
+ this.mailAddress = mailAddress;
+ }
+
+ public String getMailAddress() {
+ return mailAddress;
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/92f4a682/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/ForwardRoutes.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/ForwardRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/ForwardRoutes.java
new file mode 100644
index 0000000..ffca23c
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/ForwardRoutes.java
@@ -0,0 +1,263 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.routes;
+
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static spark.Spark.halt;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.mail.internet.AddressException;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.domainlist.api.DomainListException;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.lib.Mapping;
+import org.apache.james.rrt.lib.Mappings;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.webadmin.Constants;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.dto.ForwardDestinationResponse;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
+import org.apache.james.webadmin.utils.JsonExtractException;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.steveash.guavate.Guavate;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import spark.HaltException;
+import spark.Request;
+import spark.Response;
+import spark.Service;
+
+@Api(tags = "Address Forwards")
+@Path(ForwardRoutes.ROOT_PATH)
+@Produces(Constants.JSON_CONTENT_TYPE)
+public class ForwardRoutes implements Routes {
+
+ public static final String ROOT_PATH = "address/forwards";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ForwardRoutes.class);
+
+ private static final String FORWARD_BASE_ADDRESS = "forwardBaseAddress";
+ private static final String FORWARD_ADDRESS_PATH = ROOT_PATH + SEPARATOR + ":" + FORWARD_BASE_ADDRESS;
+ private static final String FORWARD_DESTINATION_ADDRESS = "forwardDestinationAddress";
+ private static final String USER_IN_FORWARD_DESTINATION_ADDRESSES_PATH = FORWARD_ADDRESS_PATH + SEPARATOR +
+ "targets" + SEPARATOR + ":" + FORWARD_DESTINATION_ADDRESS;
+ private static final String MAILADDRESS_ASCII_DISCLAIMER = "Note that email addresses are restricted to ASCII character set. " +
+ "Mail addresses not matching this criteria will be rejected.";
+
+ private final UsersRepository usersRepository;
+ private final JsonTransformer jsonTransformer;
+ private final RecipientRewriteTable recipientRewriteTable;
+
+ @Inject
+ @VisibleForTesting
+ ForwardRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository usersRepository, JsonTransformer jsonTransformer) {
+ this.usersRepository = usersRepository;
+ this.jsonTransformer = jsonTransformer;
+ this.recipientRewriteTable = recipientRewriteTable;
+ }
+
+ @Override
+ public void define(Service service) {
+ service.get(ROOT_PATH, this::listForwards, jsonTransformer);
+ service.get(FORWARD_ADDRESS_PATH, this::listForwardDestinations, jsonTransformer);
+ service.put(FORWARD_ADDRESS_PATH, this::throwUnknownPath);
+ service.put(USER_IN_FORWARD_DESTINATION_ADDRESSES_PATH, this::addToForwardDestinations);
+ service.delete(FORWARD_ADDRESS_PATH, this::throwUnknownPath);
+ service.delete(USER_IN_FORWARD_DESTINATION_ADDRESSES_PATH, this::removeFromForwardDestination);
+ }
+
+ public Object throwUnknownPath(Request request, Response response) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .type(ErrorType.INVALID_ARGUMENT)
+ .message("An destination address needs to be specified in the path")
+ .haltError();
+ }
+
+ @GET
+ @Path(ROOT_PATH)
+ @ApiOperation(value = "getting forwards list")
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+ @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+ message = "Internal server error - Something went bad on the server side.")
+ })
+ public Set<String> listForwards(Request request, Response response) throws RecipientRewriteTableException {
+ return Optional.ofNullable(recipientRewriteTable.getAllMappings())
+ .map(mappings ->
+ mappings.entrySet().stream()
+ .filter(e -> e.getValue().contains(Mapping.Type.Forward))
+ .map(Map.Entry::getKey)
+ .collect(Guavate.toImmutableSortedSet()))
+ .orElse(ImmutableSortedSet.of());
+ }
+
+ @PUT
+ @Path(ROOT_PATH + "/{" + FORWARD_BASE_ADDRESS + "}/targets/{" + FORWARD_DESTINATION_ADDRESS + "}")
+ @ApiOperation(value = "adding a destination address into a forward")
+ @ApiImplicitParams({
+ @ApiImplicitParam(required = true, dataType = "string", name = FORWARD_BASE_ADDRESS, paramType = "path",
+ value = "Base mail address of the forward. Sending a mail to that address will send it to all forward destinations.\n" +
+ MAILADDRESS_ASCII_DISCLAIMER),
+ @ApiImplicitParam(required = true, dataType = "string", name = FORWARD_DESTINATION_ADDRESS, paramType = "path",
+ value = "A destination mail address of the forward. Sending a mail to the base address will send an email to " +
+ "that email address (as well as other destinations).\n" +
+ MAILADDRESS_ASCII_DISCLAIMER)
+ })
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+ @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = FORWARD_BASE_ADDRESS + " or forward structure format is not valid"),
+ @ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "requested base forward address does not match a user"),
+ @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+ message = "Internal server error - Something went bad on the server side.")
+ })
+ public HaltException addToForwardDestinations(Request request, Response response) throws JsonExtractException, AddressException, RecipientRewriteTableException, UsersRepositoryException, DomainListException {
+ MailAddress forwardBaseAddress = parseMailAddress(request.params(FORWARD_BASE_ADDRESS));
+ ensureUserExist(forwardBaseAddress);
+ MailAddress destinationAddress = parseMailAddress(request.params(FORWARD_DESTINATION_ADDRESS));
+ recipientRewriteTable.addForwardMapping(forwardBaseAddress.getLocalPart(), forwardBaseAddress.getDomain(), destinationAddress.asString());
+ return halt(HttpStatus.CREATED_201);
+ }
+
+ private void ensureUserExist(MailAddress mailAddress) throws UsersRepositoryException {
+ if (!usersRepository.contains(mailAddress.asString())) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.NOT_FOUND_404)
+ .type(ErrorType.INVALID_ARGUMENT)
+ .message("Requested base forward address does not correspond to a user")
+ .haltError();
+ }
+ }
+
+
+ @DELETE
+ @Path(ROOT_PATH + "/{" + FORWARD_BASE_ADDRESS + "}/targets/{" + FORWARD_DESTINATION_ADDRESS + "}")
+ @ApiOperation(value = "remove a destination address from a forward")
+ @ApiImplicitParams({
+ @ApiImplicitParam(required = true, dataType = "string", name = FORWARD_BASE_ADDRESS, paramType = "path"),
+ @ApiImplicitParam(required = true, dataType = "string", name = FORWARD_DESTINATION_ADDRESS, paramType = "path")
+ })
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+ @ApiResponse(code = HttpStatus.BAD_REQUEST_400,
+ message = FORWARD_BASE_ADDRESS + " or forward structure format is not valid"),
+ @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+ message = "Internal server error - Something went bad on the server side.")
+ })
+ public HaltException removeFromForwardDestination(Request request, Response response) throws JsonExtractException, AddressException, RecipientRewriteTableException {
+ MailAddress baseAddress = parseMailAddress(request.params(FORWARD_BASE_ADDRESS));
+ MailAddress destinationAddressToBeRemoved = parseMailAddress(request.params(FORWARD_DESTINATION_ADDRESS));
+ recipientRewriteTable.removeForwardMapping(
+ baseAddress.getLocalPart(),
+ baseAddress.getDomain(),
+ destinationAddressToBeRemoved.asString());
+ return halt(HttpStatus.OK_200);
+ }
+
+ @GET
+ @Path(ROOT_PATH + "/{" + FORWARD_BASE_ADDRESS + "}")
+ @ApiOperation(value = "listing forward destinations")
+ @ApiImplicitParams({
+ @ApiImplicitParam(required = true, dataType = "string", name = FORWARD_BASE_ADDRESS, paramType = "path")
+ })
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = List.class),
+ @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The forward is not an address"),
+ @ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The forward does not exist"),
+ @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
+ message = "Internal server error - Something went bad on the server side.")
+ })
+ public ImmutableSet<ForwardDestinationResponse> listForwardDestinations(Request request, Response response) throws RecipientRewriteTable.ErrorMappingException, RecipientRewriteTableException {
+ MailAddress baseAddress = parseMailAddress(request.params(FORWARD_BASE_ADDRESS));
+ Mappings mappings = recipientRewriteTable.getMappings(baseAddress.getLocalPart(), baseAddress.getDomain());
+
+ ensureNonEmptyMappings(mappings);
+
+ return mappings.select(Mapping.Type.Forward)
+ .asStream()
+ .map(mapping -> mapping.asMailAddress()
+ .orElseThrow(() -> new IllegalStateException(String.format("Can not compute address for mapping %s", mapping.asString()))))
+ .map(MailAddress::asString)
+ .sorted()
+ .map(ForwardDestinationResponse::new)
+ .collect(Guavate.toImmutableSet());
+ }
+
+ private MailAddress parseMailAddress(String address) {
+ try {
+ String decodedAddress = URLDecoder.decode(address, StandardCharsets.UTF_8.displayName());
+ return new MailAddress(decodedAddress);
+ } catch (AddressException e) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .type(ErrorType.INVALID_ARGUMENT)
+ .message("The forward is not an email address")
+ .cause(e)
+ .haltError();
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.error("UTF-8 should be a valid encoding");
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .type(ErrorType.SERVER_ERROR)
+ .message("Internal server error - Something went bad on the server side.")
+ .cause(e)
+ .haltError();
+ }
+ }
+
+ private void ensureNonEmptyMappings(Mappings mappings) {
+ if (mappings == null || mappings.isEmpty()) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.NOT_FOUND_404)
+ .type(ErrorType.INVALID_ARGUMENT)
+ .message("The forward does not exist")
+ .haltError();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/james-project/blob/92f4a682/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/ForwardRoutesTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/ForwardRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/ForwardRoutesTest.java
new file mode 100644
index 0000000..11a1c10
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/ForwardRoutesTest.java
@@ -0,0 +1,676 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.routes;
+
+import static com.jayway.restassured.RestAssured.when;
+import static com.jayway.restassured.RestAssured.with;
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.configuration.DefaultConfigurationBuilder;
+import org.apache.james.core.Domain;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.metrics.logger.DefaultMetricFactory;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import com.jayway.restassured.RestAssured;
+import com.jayway.restassured.filter.log.LogDetail;
+import com.jayway.restassured.http.ContentType;
+
+class ForwardRoutesTest {
+
+ private static final Domain DOMAIN = Domain.of("b.com");
+ public static final String CEDRIC = "cedric@" + DOMAIN.name();
+ public static final String ALICE = "alice@" + DOMAIN.name();
+ public static final String ALICE_WITH_SLASH = "alice/@" + DOMAIN.name();
+ public static final String ALICE_WITH_ENCODED_SLASH = "alice%2F@" + DOMAIN.name();
+ public static final String BOB = "bob@" + DOMAIN.name();
+ public static final String BOB_PASSWORD = "123456";
+ public static final String ALICE_PASSWORD = "789123";
+ public static final String ALICE_SLASH_PASSWORD = "abcdef";
+ public static final String CEDRIC_PASSWORD = "456789";
+
+ private WebAdminServer webAdminServer;
+
+ private void createServer(ForwardRoutes forwardRoutes) throws Exception {
+ webAdminServer = WebAdminUtils.createWebAdminServer(
+ new DefaultMetricFactory(),
+ forwardRoutes);
+ webAdminServer.configure(NO_CONFIGURATION);
+ webAdminServer.await();
+
+ RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+ .setBasePath("address/forwards")
+ .log(LogDetail.METHOD)
+ .build();
+ }
+
+ @AfterEach
+ void stop() {
+ webAdminServer.destroy();
+ }
+
+ @Nested
+ class NormalBehaviour {
+
+ MemoryUsersRepository usersRepository;
+ MemoryDomainList domainList;
+ MemoryRecipientRewriteTable memoryRecipientRewriteTable;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ memoryRecipientRewriteTable = new MemoryRecipientRewriteTable();
+ DNSService dnsService = mock(DNSService.class);
+ domainList = new MemoryDomainList(dnsService);
+ domainList.setAutoDetectIP(false);
+ domainList.setAutoDetect(false);
+ domainList.configure(new DefaultConfigurationBuilder());
+ domainList.addDomain(DOMAIN);
+
+ usersRepository = MemoryUsersRepository.withVirtualHosting();
+ usersRepository.setDomainList(domainList);
+ usersRepository.configure(new DefaultConfigurationBuilder());
+
+ usersRepository.addUser(BOB, BOB_PASSWORD);
+ usersRepository.addUser(ALICE, ALICE_PASSWORD);
+ usersRepository.addUser(ALICE_WITH_SLASH, ALICE_SLASH_PASSWORD);
+ usersRepository.addUser(CEDRIC, CEDRIC_PASSWORD);
+
+ createServer(new ForwardRoutes(memoryRecipientRewriteTable, usersRepository, new JsonTransformer()));
+ }
+
+ @Test
+ void getForwardShouldBeEmpty() {
+ when()
+ .get()
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body(is("[]"));
+ }
+
+ @Test
+ void getForwardShouldListExistingForwardsInAlphabeticOrder() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ with()
+ .put(CEDRIC + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ List<String> addresses =
+ when()
+ .get()
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .extract()
+ .body()
+ .jsonPath()
+ .getList(".");
+ assertThat(addresses).containsExactly(ALICE, CEDRIC);
+ }
+
+ @Test
+ void getNotRegisteredForwardShouldReturnNotFound() {
+ Map<String, Object> errors = when()
+ .get("unknown@domain.travel")
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND_404)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.NOT_FOUND_404)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "The forward does not exist");
+ }
+
+ @Test
+ void putUserInForwardShouldReturnCreated() {
+ when()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.CREATED_201);
+ }
+
+ @Test
+ void putUserWithSlashInForwardShouldReturnCreated() {
+ when()
+ .put(BOB + SEPARATOR + "targets" + SEPARATOR + ALICE_WITH_ENCODED_SLASH)
+ .then()
+ .statusCode(HttpStatus.CREATED_201);
+ }
+
+ @Test
+ void putUserWithSlashInForwardShouldAddItAsADestination() {
+ with()
+ .put(BOB + SEPARATOR + "targets" + SEPARATOR + ALICE_WITH_ENCODED_SLASH);
+
+ when()
+ .get(BOB)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(ALICE_WITH_SLASH));
+ }
+
+ @Test
+ void putUserInForwardShouldCreateForward() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ when()
+ .get(ALICE)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(BOB));
+ }
+
+ @Test
+ void putUserInForwardWithEncodedSlashShouldReturnCreated() {
+ when()
+ .put(ALICE_WITH_ENCODED_SLASH + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.CREATED_201);
+ }
+
+ @Test
+ void putUserInForwardWithEncodedSlashShouldCreateForward() {
+ with()
+ .put(ALICE_WITH_ENCODED_SLASH + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ when()
+ .get(ALICE_WITH_ENCODED_SLASH)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(BOB));
+ }
+
+ @Test
+ void putSameUserInForwardTwiceShouldBeIdempotent() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ when()
+ .get(ALICE)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(BOB));
+ }
+
+ @Test
+ void putUserInForwardShouldAllowSeveralDestinations() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + CEDRIC);
+
+ when()
+ .get(ALICE)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(BOB, CEDRIC));
+ }
+
+ @Test
+ void forwardShouldAllowIdentity() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + ALICE);
+
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + CEDRIC);
+
+ when()
+ .get(ALICE)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(ALICE, CEDRIC));
+ }
+
+ @Test
+ void putUserInForwardShouldRequireExistingBaseUser() {
+ Map<String, Object> errors = when()
+ .put("notFound@" + DOMAIN.name() + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND_404)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.NOT_FOUND_404)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "Requested base forward address does not correspond to a user");
+ }
+
+ @Test
+ void getForwardShouldReturnMembersInAlphabeticOrder() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + CEDRIC);
+
+ when()
+ .get(ALICE)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(BOB, CEDRIC));
+ }
+
+ @Test
+ void forwardShouldAcceptExternalAddresses() {
+ String externalAddress = "external@other.com";
+
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + externalAddress);
+
+ when()
+ .get(ALICE)
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body("mailAddress", hasItems(externalAddress));
+ }
+
+ @Test
+ void deleteUserNotInForwardShouldReturnOK() {
+ when()
+ .delete(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.OK_200);
+ }
+
+ @Test
+ void deleteLastUserInForwardShouldDeleteForward() {
+ with()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ with()
+ .delete(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB);
+
+ when()
+ .get()
+ .then()
+ .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.OK_200)
+ .body(is("[]"));
+ }
+ }
+
+ @Nested
+ class FilteringOtherRewriteRuleTypes extends NormalBehaviour {
+
+ @BeforeEach
+ void setup() throws Exception {
+ super.setUp();
+ memoryRecipientRewriteTable.addErrorMapping("error", DOMAIN, "disabled");
+ memoryRecipientRewriteTable.addRegexMapping("regex", DOMAIN, ".*@b\\.com");
+ memoryRecipientRewriteTable.addAliasDomainMapping(Domain.of("alias"), DOMAIN);
+ }
+
+ }
+
+ @Nested
+ class ExceptionHandling {
+
+ private RecipientRewriteTable memoryRecipientRewriteTable;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ memoryRecipientRewriteTable = mock(RecipientRewriteTable.class);
+ UsersRepository userRepository = mock(UsersRepository.class);
+ Mockito.when(userRepository.contains(eq(ALICE))).thenReturn(true);
+ DomainList domainList = mock(DomainList.class);
+ Mockito.when(domainList.containsDomain(any())).thenReturn(true);
+ createServer(new ForwardRoutes(memoryRecipientRewriteTable, userRepository, new JsonTransformer()));
+ }
+
+ @Test
+ void getMalformedForwardShouldReturnBadRequest() {
+ Map<String, Object> errors = when()
+ .get("not-an-address")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "The forward is not an email address")
+ .containsEntry("cause", "Out of data at position 1 in 'not-an-address'");
+ }
+
+ @Test
+ void putMalformedForwardShouldReturnBadRequest() {
+ Map<String, Object> errors = when()
+ .put("not-an-address" + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "The forward is not an email address")
+ .containsEntry("cause", "Out of data at position 1 in 'not-an-address'");
+ }
+
+ @Test
+ void putUserInForwardWithSlashShouldReturnNotFound() {
+ when()
+ .put(ALICE_WITH_SLASH + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND_404)
+ .body(containsString("404 Not found"));
+ }
+
+ @Test
+ void putUserWithSlashInForwardShouldReturnNotFound() {
+ when()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + ALICE_WITH_SLASH)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND_404)
+ .body(containsString("404 Not found"));
+ }
+
+ @Test
+ void putMalformedAddressShouldReturnBadRequest() {
+ Map<String, Object> errors = when()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + "not-an-address")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "The forward is not an email address")
+ .containsEntry("cause", "Out of data at position 1 in 'not-an-address'");
+ }
+
+ @Test
+ void putRequiresTwoPathParams() {
+ when()
+ .put(ALICE)
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .body(is(""));
+ }
+
+ @Test
+ void deleteMalformedForwardShouldReturnBadRequest() {
+ Map<String, Object> errors = when()
+ .delete("not-an-address" + SEPARATOR + "targets" + SEPARATOR + ALICE)
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "The forward is not an email address")
+ .containsEntry("cause", "Out of data at position 1 in 'not-an-address'");
+ }
+
+ @Test
+ void deleteMalformedAddressShouldReturnBadRequest() {
+ Map<String, Object> errors = when()
+ .delete(ALICE + SEPARATOR + "targets" + SEPARATOR + "not-an-address")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .extract()
+ .body()
+ .jsonPath()
+ .getMap(".");
+
+ assertThat(errors)
+ .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
+ .containsEntry("type", "InvalidArgument")
+ .containsEntry("message", "The forward is not an email address")
+ .containsEntry("cause", "Out of data at position 1 in 'not-an-address'");
+ }
+
+ @Test
+ void deleteRequiresTwoPathParams() {
+ when()
+ .delete(ALICE)
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .body(is(""));
+ }
+
+ @Test
+ void putShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTableException.class)
+ .when(memoryRecipientRewriteTable)
+ .addForwardMapping(anyString(), any(), anyString());
+
+ when()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void putShouldReturnErrorWhenErrorMappingExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTable.ErrorMappingException.class)
+ .when(memoryRecipientRewriteTable)
+ .addForwardMapping(anyString(), any(), anyString());
+
+ when()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void putShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+ doThrow(RuntimeException.class)
+ .when(memoryRecipientRewriteTable)
+ .addForwardMapping(anyString(), any(), anyString());
+
+ when()
+ .put(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void getAllShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTableException.class)
+ .when(memoryRecipientRewriteTable)
+ .getAllMappings();
+
+ when()
+ .get()
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void getAllShouldReturnErrorWhenErrorMappingExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTable.ErrorMappingException.class)
+ .when(memoryRecipientRewriteTable)
+ .getAllMappings();
+
+ when()
+ .get()
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void getAllShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+ doThrow(RuntimeException.class)
+ .when(memoryRecipientRewriteTable)
+ .getAllMappings();
+
+ when()
+ .get()
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void deleteShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTableException.class)
+ .when(memoryRecipientRewriteTable)
+ .removeForwardMapping(anyString(), any(), anyString());
+
+ when()
+ .delete(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void deleteShouldReturnErrorWhenErrorMappingExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTable.ErrorMappingException.class)
+ .when(memoryRecipientRewriteTable)
+ .removeForwardMapping(anyString(), any(), anyString());
+
+ when()
+ .delete(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void deleteShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+ doThrow(RuntimeException.class)
+ .when(memoryRecipientRewriteTable)
+ .removeForwardMapping(anyString(), any(), anyString());
+
+ when()
+ .delete(ALICE + SEPARATOR + "targets" + SEPARATOR + BOB)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void getShouldReturnErrorWhenRecipientRewriteTableExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTableException.class)
+ .when(memoryRecipientRewriteTable)
+ .getMappings(anyString(), any());
+
+ when()
+ .get(ALICE)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void getShouldReturnErrorWhenErrorMappingExceptionIsThrown() throws Exception {
+ doThrow(RecipientRewriteTable.ErrorMappingException.class)
+ .when(memoryRecipientRewriteTable)
+ .getMappings(anyString(), any());
+
+ when()
+ .get(ALICE)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+
+ @Test
+ void getShouldReturnErrorWhenRuntimeExceptionIsThrown() throws Exception {
+ doThrow(RuntimeException.class)
+ .when(memoryRecipientRewriteTable)
+ .getMappings(anyString(), any());
+
+ when()
+ .get(ALICE)
+ .then()
+ .statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
+ .body(containsString("500 Internal Server Error"));
+ }
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org