This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch droplist
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/droplist by this push:
     new cb474ea101 JAMES-3946 add WebAdmin API to manage the DropList (#2241)
cb474ea101 is described below

commit cb474ea10185f41e007670e979dcdad9b0d75969
Author: Maksim <85022218+maxxx...@users.noreply.github.com>
AuthorDate: Tue May 14 10:29:52 2024 +0300

    JAMES-3946 add WebAdmin API to manage the DropList (#2241)
---
 .../docs/modules/ROOT/pages/operate/webadmin.adoc  | 120 +++++++++
 .../james/webadmin/routes/DropListRoutes.java      | 218 ++++++++++++++++
 .../james/webadmin/routes/DropListRoutesTest.java  | 273 +++++++++++++++++++++
 3 files changed, 611 insertions(+)

diff --git 
a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc 
b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
index 81ab4f0c00..0186ff42b0 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
@@ -2835,6 +2835,126 @@ Response codes:
 
 * 204: Operation succeeded
 
+== Administrating DropLists
+
+The DropList, also known as the mail blacklist, is a collection of
+domains and email addresses that are denied from sending emails within the 
system.
+
+Owner scopes:
+
+- *global*: contains entries that are blocked across all domains and addresses 
within the system.
+Entries in the global owner scope apply universally and affect all users and 
domains.
+- *domain*: each domain can have its own droplist, which contains entries 
specific to that domain.
+- *user*: allow to customize personalized droplist of blocked domains and 
email addresses.
+
+The `deniedEntityType` query parameter is optional and can take the values 
`domain` or `address`.
+
+=== Getting the DropList
+==== Global DropList
+....
+curl -XGET http://ip:port/droplist/global?deniedEntityType=null|domain|address
+....
+==== Domain DropList
+....
+curl -XGET 
http://ip:port/droplist/domain/target.com?deniedEntityType=null|domain|address
+....
+==== User DropList
+....
+curl -XGET 
http://ip:port/droplist/user/tag...@target.com?deniedEntityType=null|domain|address
+....
+
+The answer looks like:
+....
+[ "evil.com", "devil.com", "bad_...@crime.com", "hac...@murder.org" ]
+....
+
+Response codes:
+
+* 200: The drop list was successfully retrieved
+* 400: Invalid `owner scope` or `deniedEntityType`
+
+=== Testing a denied entity existence
+==== Global DropList
+....
+curl -XHEAD http://ip:port/droplist/global/attac...@evil.com
+....
+....
+curl -XHEAD http://ip:port/droplist/global/evil.com
+....
+==== Domain DropList
+....
+curl -XHEAD http://ip:port/droplist/domain/target.com/attac...@evil.com
+....
+....
+curl -XHEAD http://ip:port/droplist/domain/target.com/evil.com
+....
+==== User DropList
+....
+curl -XHEAD http://ip:port/droplist/user/tar...@target.com/attac...@evil.com
+....
+....
+curl -XHEAD http://ip:port/droplist/user/tar...@target.com/evil.com
+....
+Response codes:
+
+* 200: The denied entity exists
+* 404: The denied entity does not exist
+
+=== Add Entry to the DropList
+The denied entity must be a valid email address or 
link:#_create_a_domain[domain].
+
+==== Global DropList
+....
+curl -XPUT http://ip:port/droplist/global/attac...@evil.com
+....
+....
+curl -XPUT http://ip:port/droplist/global/evil.com
+....
+==== Domain DropList
+....
+curl -XPUT http://ip:port/droplist/domain/target.com/attac...@evil.com
+....
+....
+curl -XPUT http://ip:port/droplist/domain/target.com/evil.com
+....
+==== User DropList
+....
+curl -XPUT http://ip:port/droplist/user/tar...@target.com/attac...@evil.com
+....
+....
+curl -XPUT http://ip:port/droplist/user/tar...@target.com/evil.com
+....
+Response codes:
+
+* 204: The denied entity was successfully added
+* 400: The denied entity is invalid
+
+=== Remove Entry from the DropList
+==== Global DropList
+....
+curl -XDELETE http://ip:port/droplist/global/attac...@evil.com
+....
+....
+curl -XDELETE http://ip:port/droplist/global/evil.com
+....
+==== Domain DropList
+....
+curl -XDELETE http://ip:port/droplist/domain/target.com/attac...@evil.com
+....
+....
+curl -XDELETE http://ip:port/droplist/domain/target.com/evil.com
+....
+==== User DropList
+....
+curl -XDELETE http://ip:port/droplist/user/tar...@target.com/attac...@evil.com
+....
+....
+curl -XDELETE http://ip:port/droplist/user/tar...@target.com/evil.com
+....
+Response codes:
+
+* 204: Entry deleted successfully.
+
 == Administrating Jmap Uploads
 
 === Cleaning upload repository
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DropListRoutes.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DropListRoutes.java
new file mode 100644
index 0000000000..5fdb93f28e
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DropListRoutes.java
@@ -0,0 +1,218 @@
+/****************************************************************
+ * 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 java.util.List;
+import java.util.Optional;
+
+import jakarta.inject.Inject;
+import jakarta.mail.internet.AddressException;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.james.core.Domain;
+import org.apache.james.core.MailAddress;
+import org.apache.james.droplists.api.DeniedEntityType;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+import org.apache.james.util.ReactorUtils;
+import org.apache.james.webadmin.Constants;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
+import org.eclipse.jetty.http.HttpStatus;
+
+import com.google.common.collect.ImmutableSet;
+
+import reactor.core.publisher.Flux;
+import spark.Request;
+import spark.Response;
+import spark.Service;
+
+public class DropListRoutes implements Routes {
+    public static final String DROP_LIST = "/droplist";
+    public static final String OWNER_SCOPE = ":ownerScope";
+    public static final String OWNER = ":owner";
+    public static final String DENIED_ENTITY = ":deniedEntity";
+    public static final String DENIED_ENTITY_TYPE = "deniedEntityType";
+
+    private final DropList dropList;
+    private final JsonTransformer jsonTransformer;
+
+    @Inject
+    public DropListRoutes(DropList dropList, JsonTransformer jsonTransformer) {
+        this.dropList = dropList;
+        this.jsonTransformer = jsonTransformer;
+    }
+
+    @Override
+    public String getBasePath() {
+        return DROP_LIST;
+    }
+
+    @Override
+    public void define(Service service) {
+        service.get(DROP_LIST + SEPARATOR + OWNER_SCOPE, this::getDropList, 
jsonTransformer);
+        service.get(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER, 
this::getDropList, jsonTransformer);
+        service.put(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER + 
SEPARATOR + DENIED_ENTITY, this::addDropListEntry);
+        service.put(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + 
DENIED_ENTITY, this::addDropListEntry);
+        service.head(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER + 
SEPARATOR + DENIED_ENTITY, this::dropListEntryExist);
+        service.head(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + 
DENIED_ENTITY, this::dropListEntryExist);
+        service.delete(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER 
+ SEPARATOR + DENIED_ENTITY, this::removeDropListEntry);
+        service.delete(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + 
DENIED_ENTITY, this::removeDropListEntry);
+    }
+
+    public ImmutableSet<String> getDropList(Request request, Response 
response) {
+        OwnerScope ownerScope = 
checkValidOwnerScope(request.params(OWNER_SCOPE));
+        String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+        Optional<DeniedEntityType> deniedEntityType = 
checkValidDeniedEntityType(request.queryParams(DENIED_ENTITY_TYPE));
+        if (deniedEntityType.isPresent()) {
+            return dropList.list(ownerScope, owner)
+                .filter(deniedEntry -> 
deniedEntry.getDeniedEntityType().equals(deniedEntityType.get()))
+                .map(DropListEntry::getDeniedEntity)
+                .collect(ImmutableSet.toImmutableSet())
+                .block();
+        } else {
+            return dropList.list(ownerScope, owner)
+                .map(DropListEntry::getDeniedEntity)
+                .collect(ImmutableSet.toImmutableSet())
+                .block();
+        }
+    }
+
+    public String addDropListEntry(Request request, Response response) {
+        OwnerScope ownerScope = 
checkValidOwnerScope(request.params(OWNER_SCOPE));
+        String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+        String deniedEntity = request.params(DENIED_ENTITY);
+        DropListEntry dropListEntry = getDropListEntry(ownerScope, owner, 
deniedEntity);
+        dropList.add(dropListEntry).block();
+        return Responses.returnNoContent(response);
+    }
+
+    public String removeDropListEntry(Request request, Response response) {
+        OwnerScope ownerScope = 
checkValidOwnerScope(request.params(OWNER_SCOPE));
+        String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+        String deniedEntity = request.params(DENIED_ENTITY);
+        dropList.list(ownerScope, owner)
+            .filter(dropListEntry -> 
dropListEntry.getDeniedEntity().equals(deniedEntity))
+            .collectList()
+            .doOnNext(this::deleteDropListEntry)
+            .block();
+        return Responses.returnNoContent(response);
+    }
+
+    public String dropListEntryExist(Request request, Response response) {
+        OwnerScope ownerScope = 
checkValidOwnerScope(request.params(OWNER_SCOPE));
+        String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+        String deniedEntity = request.params(DENIED_ENTITY);
+        boolean entryExists = dropList.list(ownerScope, owner)
+            .any(dropListEntry -> 
dropListEntry.getDeniedEntity().equals(deniedEntity))
+            .block();
+        if (entryExists) {
+            response.status(HttpStatus.NO_CONTENT_204);
+        } else {
+            response.status(HttpStatus.NOT_FOUND_404);
+        }
+        return Constants.EMPTY_BODY;
+    }
+
+    private Optional<DeniedEntityType> checkValidDeniedEntityType(String 
deniedEntityType) {
+        try {
+            if (deniedEntityType == null || deniedEntityType.isEmpty()) {
+                return Optional.empty();
+            } else {
+                return 
Optional.of(DeniedEntityType.valueOf(deniedEntityType.toUpperCase()));
+            }
+        } catch (IllegalArgumentException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid DeniedEntityType")
+                .cause(new IllegalArgumentException("DeniedEntityType '" + 
deniedEntityType + "' is invalid. Supported values are " +
+                    EnumUtils.getEnumList(DeniedEntityType.class)))
+                .haltError();
+        }
+    }
+
+    private DropListEntry getDropListEntry(OwnerScope ownerScope, String 
owner, String deniedEntity) {
+        DropListEntry.Builder dropListEntryBuilder = DropListEntry.builder();
+        switch (ownerScope) {
+            case GLOBAL -> dropListEntryBuilder = 
dropListEntryBuilder.forAll();
+            case DOMAIN -> dropListEntryBuilder = 
dropListEntryBuilder.domainOwner(checkValidDomain(owner));
+            case USER -> dropListEntryBuilder = 
dropListEntryBuilder.userOwner(checkValidMailAddress(owner));
+        }
+        if (deniedEntity.contains("@")) {
+            
dropListEntryBuilder.denyAddress(checkValidMailAddress(deniedEntity));
+        } else {
+            dropListEntryBuilder.denyDomain(checkValidDomain(deniedEntity));
+        }
+        return dropListEntryBuilder.build();
+    }
+
+    private OwnerScope checkValidOwnerScope(String ownerScope) {
+        try {
+            return OwnerScope.valueOf(ownerScope.toUpperCase());
+        } catch (IllegalArgumentException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid OwnerScope")
+                .cause(new IllegalArgumentException("OwnerScope '" + 
ownerScope + "' is invalid. Supported values are " +
+                    EnumUtils.getEnumList(OwnerScope.class)))
+                .haltError();
+        }
+    }
+
+    private static MailAddress checkValidMailAddress(String address) {
+        try {
+            return new MailAddress(address);
+        } catch (AddressException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid mail address %s", address)
+                .cause(e)
+                .haltError();
+        }
+    }
+
+    private Domain checkValidDomain(String domainName) {
+        try {
+            return Domain.of(domainName);
+        } catch (IllegalArgumentException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("Invalid domain %s", domainName)
+                .cause(e)
+                .haltError();
+        }
+    }
+
+    private void deleteDropListEntry(List<DropListEntry> dropListEntries) {
+        Flux.fromIterable(dropListEntries)
+            .flatMap(dropList::remove, ReactorUtils.DEFAULT_CONCURRENCY)
+            .then()
+            .block();
+    }
+}
\ No newline at end of file
diff --git 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DropListRoutesTest.java
 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DropListRoutesTest.java
new file mode 100644
index 0000000000..3ba3df5d2e
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DropListRoutesTest.java
@@ -0,0 +1,273 @@
+/****************************************************************
+ * 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 io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.is;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import jakarta.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+import org.apache.james.droplists.memory.MemoryDropList;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.utils.ErrorResponder;
+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.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+
+class DropListRoutesTest {
+    private static final String DENIED_SENDER = "attac...@evil.com";
+    private static final String OWNER_RECIPIENT = "ow...@owner.com";
+
+    private WebAdminServer webAdminServer;
+    private DropList dropList;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        dropList = new MemoryDropList();
+        DropListRoutes dropListRoutes = new DropListRoutes(dropList, new 
JsonTransformer());
+        this.webAdminServer = 
WebAdminUtils.createWebAdminServer(dropListRoutes).start();
+        RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer).build();
+        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+        getDropListTestEntries().forEach(entry -> dropList.add(entry).block());
+    }
+
+    @AfterEach
+    void tearDown() throws AddressException {
+        webAdminServer.destroy();
+        getDropListTestEntries().forEach(entry -> 
dropList.remove(entry).block());
+    }
+
+    @ParameterizedTest(name = "{index} Owner: {0}")
+    @ValueSource(strings = {
+        "global",
+        "domain/owner.com",
+        "user/ow...@owner.com"})
+    void shouldGetFullDropList(String pathParam) {
+        when()
+            .get(DropListRoutes.DROP_LIST + SEPARATOR + pathParam)
+        .then()
+            .statusCode(HttpStatus.OK_200)
+            .contentType(ContentType.JSON)
+            .body(".", containsInAnyOrder("attac...@evil.com", "evil.com"));
+    }
+
+    @ParameterizedTest(name = "{index} Owner: {0}")
+    @ValueSource(strings = {
+        "unknown",
+        "unknown/owner.com",
+        "unknown/ow...@owner.com"})
+    void shouldHandleWhenGetDropListWithInvalidOwnerScope(String pathParam) {
+        when()
+            .get(DropListRoutes.DROP_LIST + SEPARATOR + pathParam)
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .contentType(ContentType.JSON)
+            .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+            .body("type", 
is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid OwnerScope"))
+            .body("details", is("OwnerScope 'unknown' is invalid. Supported 
values are [GLOBAL, DOMAIN, USER]"));
+    }
+
+    @ParameterizedTest(name = "{index} Owner: {0}, DeniedEntityType: {1}")
+    @CsvSource(value = {
+        "global, domain, evil.com",
+        "global, address, attac...@evil.com",
+        "domain/owner.com, domain, evil.com",
+        "domain/owner.com, address, attac...@evil.com",
+        "user/ow...@owner.com, domain, evil.com",
+        "user/ow...@owner.com, address, attac...@evil.com"})
+    void shouldGetDropListWithQueryParams(String pathParam, String queryParam, 
String expected) {
+        given()
+            .queryParam("deniedEntityType", queryParam)
+        .when()
+            .get(DropListRoutes.DROP_LIST + SEPARATOR + pathParam)
+        .then()
+            .statusCode(HttpStatus.OK_200)
+            .contentType(ContentType.JSON)
+            .body(".", containsInAnyOrder(expected));
+    }
+
+    @Test
+    void shouldHandleInvalidDeniedEntityType() {
+        given()
+            .queryParam("deniedEntityType", "unknown")
+        .when()
+            .get(DropListRoutes.DROP_LIST + SEPARATOR + "global")
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .contentType(ContentType.JSON)
+            .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+            .body("type", 
is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Invalid DeniedEntityType"))
+            .body("details", is("DeniedEntityType 'unknown' is invalid. 
Supported values are [ADDRESS, DOMAIN]"));
+    }
+
+    @ParameterizedTest(name = "{index} OwnerScope: {0}, Owner: {1}, 
DeniedEntity: {2}")
+    @CsvSource(value = {
+        "global, , devil.com",
+        "global, , bad_...@crime.com",
+        "domain, owner.com, devil.com",
+        "domain, owner.com, bad_...@crime.com",
+        "user, ow...@owner.com, devil.com",
+        "user, ow...@owner.com, bad_...@crime.com"})
+    void shouldAddDropListEntry(String ownerScope, String owner, String 
newDeniedEntity) {
+        when()
+            .put(DropListRoutes.DROP_LIST + SEPARATOR + ownerScope + SEPARATOR 
+ owner + SEPARATOR + newDeniedEntity)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(getResultDropList(ownerScope, 
owner)).contains(newDeniedEntity);
+    }
+
+    @ParameterizedTest(name = "{index} OwnerScope: {0}, Owner: {1}, 
DeniedEntity: {2}")
+    @CsvSource(value = {
+        "global, , devil..com, Invalid domain devil..com",
+        "global, , bad_guy@@crime.com, Invalid mail address 
bad_guy@@crime.com",
+        "domain, owner.com, devil..com, Invalid domain devil..com",
+        "domain, owner.com, bad_guy@@crime.com, Invalid mail address 
bad_guy@@crime.com",
+        "user, ow...@owner.com, devil..com, Invalid domain devil..com",
+        "user, ow...@owner.com, bad_guy@@crime.com, Invalid mail address 
bad_guy@@crime.com"})
+    void shouldFailWhenAddInvalidDeniedEntity(String ownerScope, String owner, 
String newDeniedEntity, String message) {
+        when()
+            .put(DropListRoutes.DROP_LIST + SEPARATOR + ownerScope + SEPARATOR 
+ owner + SEPARATOR + newDeniedEntity)
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .contentType(ContentType.JSON)
+            .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+            .body("type", 
is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is(message));
+    }
+
+    @ParameterizedTest(name = "{index} Path: {0}")
+    @CsvSource(value = {
+        "/global/evil.com",
+        "/global/attac...@evil.com",
+        "/domain/owner.com/evil.com",
+        "/domain/owner.com/attac...@evil.com",
+        "/user/ow...@owner.com/evil.com",
+        "/user/ow...@owner.com/attac...@evil.com"})
+    void headShouldReturnNoContentWhenDomainDeniedEntityExists(String path) {
+        when()
+            .head(DropListRoutes.DROP_LIST + path)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+    }
+
+    @ParameterizedTest(name = "{index} Path: {0}")
+    @CsvSource(value = {
+        "global/devil.com",
+        "global/bad_...@crime.com",
+        "/domain/owner.com/devil.com",
+        "/domain/owner.com/bad_...@crime.com",
+        "/user/ow...@owner.com/devil.com",
+        "/user/ow...@owner.com/bad_...@crime.com"})
+    void headShouldReturnNotFoundWhenDomainDeniedEntityNotExists(String path) {
+        when()
+            .head(DropListRoutes.DROP_LIST + path)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @ParameterizedTest(name = "{index} Path: {3}")
+    @CsvSource(value = {
+        "global, , evil.com, /global/evil.com",
+        "global, , attac...@evil.com, /global/attac...@evil.com",
+        "domain, owner.com, evil.com, /domain/owner.com/evil.com",
+        "domain, owner.com, attac...@evil.com, 
/domain/owner.com/attac...@evil.com",
+        "user, ow...@owner.com, evil.com, /user/ow...@owner.com/evil.com",
+        "user, ow...@owner.com, attac...@evil.com, 
/user/ow...@owner.com/attac...@evil.com"})
+    void deleteShouldReturnNoContent(String ownerScope, String owner, String 
deniedEntity, String path) {
+        given()
+            .delete(DropListRoutes.DROP_LIST + path)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(getResultDropList(ownerScope, 
owner)).doesNotContain(deniedEntity);
+    }
+
+    @Test
+    void deleteShouldReturnNotFoundWhenUsedWithEmptyEntry() {
+        given()
+            .delete(SEPARATOR)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    static Stream<DropListEntry> getDropListTestEntries() throws 
AddressException {
+        return Stream.of(
+            DropListEntry.builder()
+                .forAll()
+                .denyAddress(new MailAddress(DENIED_SENDER))
+                .build(),
+            DropListEntry.builder()
+                .forAll()
+                .denyDomain(new MailAddress(DENIED_SENDER).getDomain())
+                .build(),
+            DropListEntry.builder()
+                .forAll()
+                .denyAddress(new MailAddress(DENIED_SENDER))
+                .build(),
+            DropListEntry.builder()
+                .domainOwner(new MailAddress(OWNER_RECIPIENT).getDomain())
+                .denyAddress(new MailAddress(DENIED_SENDER))
+                .build(),
+            DropListEntry.builder()
+                .domainOwner(new MailAddress(OWNER_RECIPIENT).getDomain())
+                .denyDomain(new MailAddress(DENIED_SENDER).getDomain())
+                .build(),
+            DropListEntry.builder()
+                .userOwner(new MailAddress(OWNER_RECIPIENT))
+                .denyAddress(new MailAddress(DENIED_SENDER))
+                .build(),
+            DropListEntry.builder()
+                .userOwner(new MailAddress(OWNER_RECIPIENT))
+                .denyDomain(new MailAddress(DENIED_SENDER).getDomain())
+                .build());
+    }
+
+    private List<String> getResultDropList(String ownerScope, String owner) {
+        return dropList.list(OwnerScope.valueOf(ownerScope.toUpperCase()),
+                Optional.ofNullable(owner).orElse(""))
+            .map(DropListEntry::getDeniedEntity)
+            .collectList()
+            .block();
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to