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 ro...@apache.org on 2018/02/27 13:27:44 UTC

[4/5] james-project git commit: JAMES-2344 First implementation of admin API for User Quota

JAMES-2344 First implementation of admin API for User Quota


Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/e87c0553
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/e87c0553
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/e87c0553

Branch: refs/heads/master
Commit: e87c05530de5d53d1d38d83fce86717cd2ef6f79
Parents: 159886f
Author: Matthieu Baechler <ma...@apache.org>
Authored: Tue Feb 20 18:39:52 2018 +0100
Committer: Matthieu Baechler <ma...@apache.org>
Committed: Tue Feb 27 09:59:23 2018 +0100

----------------------------------------------------------------------
 .../apache/james/mailbox/model/QuotaRoot.java   |   6 +
 .../protocols/webadmin/webadmin-mailbox/pom.xml |   6 +
 .../james/webadmin/routes/UserQuotaRoutes.java  | 281 ++++++++++++
 .../webadmin/service/UserQuotaService.java      |  77 ++++
 .../webadmin/routes/UserQuotaRoutesTest.java    | 457 +++++++++++++++++++
 5 files changed, 827 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/e87c0553/mailbox/api/src/main/java/org/apache/james/mailbox/model/QuotaRoot.java
----------------------------------------------------------------------
diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/QuotaRoot.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/QuotaRoot.java
index 885540b..debb37a 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/QuotaRoot.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/QuotaRoot.java
@@ -26,6 +26,12 @@ import com.google.common.base.Objects;
  */
 public class QuotaRoot {
 
+    private static final String USER = "user-";
+
+    public static QuotaRoot forUser(String value) {
+        return new QuotaRoot(USER + value);
+    }
+
     public static QuotaRoot quotaRoot(String value) {
         return new QuotaRoot(value);
     }

http://git-wip-us.apache.org/repos/asf/james-project/blob/e87c0553/server/protocols/webadmin/webadmin-mailbox/pom.xml
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-mailbox/pom.xml b/server/protocols/webadmin/webadmin-mailbox/pom.xml
index dfe3820..bfed1ee 100644
--- a/server/protocols/webadmin/webadmin-mailbox/pom.xml
+++ b/server/protocols/webadmin/webadmin-mailbox/pom.xml
@@ -35,6 +35,12 @@
     <dependencies>
         <dependency>
             <groupId>${project.groupId}</groupId>
+            <artifactId>james-server-dnsservice-api</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
             <artifactId>apache-james-mailbox-api</artifactId>
         </dependency>
         <dependency>

http://git-wip-us.apache.org/repos/asf/james-project/blob/e87c0553/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java
new file mode 100644
index 0000000..b72d65f
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/routes/UserQuotaRoutes.java
@@ -0,0 +1,281 @@
+/****************************************************************
+ * 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 javax.inject.Inject;
+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.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.dto.QuotaDTO;
+import org.apache.james.webadmin.dto.QuotaRequest;
+import org.apache.james.webadmin.service.UserQuotaService;
+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.JsonExtractor;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.Request;
+import spark.Service;
+
+@Api(tags = "UserQuota")
+@Path(UserQuotaRoutes.QUOTA_ENDPOINT)
+@Produces("application/json")
+public class UserQuotaRoutes implements Routes {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Routes.class);
+
+    private static final String USER = "user";
+    static final String QUOTA_ENDPOINT = "/quota/users/:" + USER;
+    private static final String COUNT_ENDPOINT = QUOTA_ENDPOINT + "/count";
+    private static final String SIZE_ENDPOINT = QUOTA_ENDPOINT + "/size";
+
+    private final UsersRepository usersRepository;
+    private final UserQuotaService userQuotaService;
+    private final JsonTransformer jsonTransformer;
+    private final JsonExtractor<QuotaDTO> jsonExtractor;
+    private Service service;
+
+    @Inject
+    public UserQuotaRoutes(UsersRepository usersRepository, UserQuotaService userQuotaService, JsonTransformer jsonTransformer) {
+        this.usersRepository = usersRepository;
+        this.userQuotaService = userQuotaService;
+        this.jsonTransformer = jsonTransformer;
+        this.jsonExtractor = new JsonExtractor<>(QuotaDTO.class);
+    }
+
+    @Override
+    public void define(Service service) {
+        this.service = service;
+
+        defineGetQuotaCount();
+        defineDeleteQuotaCount();
+        defineUpdateQuotaCount();
+
+        defineGetQuotaSize();
+        defineDeleteQuotaSize();
+        defineUpdateQuotaSize();
+
+        defineGetQuota();
+        defineUpdateQuota();
+    }
+
+    @PUT
+    @ApiOperation(value = "Updating count and size at the same time")
+    @ApiImplicitParams({
+            @ApiImplicitParam(required = true, dataType = "org.apache.james.webadmin.dto.QuotaDTO", paramType = "body")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK. The value has been updated."),
+            @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The body is not a positive integer or not unlimited value (-1)."),
+            @ApiResponse(code = HttpStatus.CONFLICT_409, message = "The requested restriction can't be enforced right now."),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineUpdateQuota() {
+        service.put(QUOTA_ENDPOINT, ((request, response) -> {
+            String user = checkUserExist(request);
+            QuotaDTO quotaDTO = parseQuotaDTO(request);
+            userQuotaService.defineQuota(user, quotaDTO);
+            response.status(HttpStatus.NO_CONTENT_204);
+            return response;
+        }));
+    }
+
+    @GET
+    @ApiOperation(
+        value = "Reading count and size at the same time",
+        notes = "If there is no limitation for count and/or size, the returned value will be -1"
+    )
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = QuotaDTO.class),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineGetQuota() {
+        service.get(QUOTA_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            return userQuotaService.getQuota(user);
+        }, jsonTransformer);
+    }
+
+    @DELETE
+    @Path("/size")
+    @ApiOperation(value = "Removing per user mail size limitation by updating to unlimited value")
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "The value is updated to unlimited value."),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineDeleteQuotaSize() {
+        service.delete(SIZE_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            userQuotaService.deleteMaxSizeQuota(user);
+            response.status(HttpStatus.NO_CONTENT_204);
+            return response;
+        });
+    }
+
+    @PUT
+    @Path("/size")
+    @ApiOperation(value = "Updating per user mail size limitation")
+    @ApiImplicitParams({
+            @ApiImplicitParam(required = true, dataType = "integer", paramType = "body")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK. The value has been updated."),
+            @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The body is not a positive integer."),
+            @ApiResponse(code = HttpStatus.CONFLICT_409, message = "The requested restriction can't be enforced right now."),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineUpdateQuotaSize() {
+        service.put(SIZE_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            QuotaRequest quotaRequest = parseQuotaRequest(request);
+            userQuotaService.defineMaxSizeQuota(user, quotaRequest);
+            response.status(HttpStatus.NO_CONTENT_204);
+            return response;
+        });
+    }
+
+    @GET
+    @Path("/size")
+    @ApiOperation(value = "Reading per user mail size limitation")
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = Long.class),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineGetQuotaSize() {
+        service.get(SIZE_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            return userQuotaService.getMaxSizeQuota(user);
+        }, jsonTransformer);
+    }
+
+    @DELETE
+    @Path("/count")
+    @ApiOperation(value = "Removing per user mail count limitation by updating to unlimited value")
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "The value is updated to unlimited value."),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineDeleteQuotaCount() {
+        service.delete(COUNT_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            userQuotaService.deleteMaxCountQuota(user);
+            response.status(HttpStatus.NO_CONTENT_204);
+            return response;
+        });
+    }
+
+    @PUT
+    @Path("/count")
+    @ApiOperation(value = "Updating per user mail count limitation")
+    @ApiImplicitParams({
+            @ApiImplicitParam(required = true, dataType = "integer", paramType = "body")
+    })
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK. The value has been updated."),
+            @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The body is not a positive integer."),
+            @ApiResponse(code = HttpStatus.CONFLICT_409, message = "The requested restriction can't be enforced right now."),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineUpdateQuotaCount() {
+        service.put(COUNT_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            QuotaRequest quotaRequest = parseQuotaRequest(request);
+            userQuotaService.defineMaxCountQuota(user, quotaRequest);
+            response.status(HttpStatus.NO_CONTENT_204);
+            return response;
+        });
+    }
+
+    @GET
+    @Path("/count")
+    @ApiOperation(value = "Reading per user mail count limitation")
+    @ApiResponses(value = {
+            @ApiResponse(code = HttpStatus.OK_200, message = "OK", response = Long.class),
+            @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
+    })
+    public void defineGetQuotaCount() {
+        service.get(COUNT_ENDPOINT, (request, response) -> {
+            String user = checkUserExist(request);
+            return userQuotaService.getMaxCountQuota(user);
+        }, jsonTransformer);
+    }
+
+    private String checkUserExist(Request request) throws UsersRepositoryException {
+        String user = request.params(USER);
+        if (!usersRepository.contains(user)) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.NOT_FOUND_404)
+                .type(ErrorType.NOT_FOUND)
+                .message("User not found")
+                .haltError();
+        }
+        return user;
+    }
+
+    private QuotaDTO parseQuotaDTO(Request request) {
+        try {
+            return jsonExtractor.parse(request.body());
+        } catch (IllegalArgumentException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorType.INVALID_ARGUMENT)
+                .message("Quota should be positive or unlimited (-1)")
+                .cause(e)
+                .haltError();
+        } catch (JsonExtractException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorType.INVALID_ARGUMENT)
+                .message("Malformed JSON input")
+                .cause(e)
+                .haltError();
+        }
+    }
+
+    private QuotaRequest parseQuotaRequest(Request request) {
+        try {
+            return QuotaRequest.parse(request.body());
+        } catch (IllegalArgumentException e) {
+            LOGGER.info("Invalid quota. Need to be an integer value greater than 0");
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorType.INVALID_ARGUMENT)
+                .message("Invalid quota. Need to be an integer value greater than 0")
+                .cause(e)
+                .haltError();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/e87c0553/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java
new file mode 100644
index 0000000..ddaf630
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/UserQuotaService.java
@@ -0,0 +1,77 @@
+/****************************************************************
+ * 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.service;
+
+import javax.inject.Inject;
+
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.Quota;
+import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.mailbox.quota.MaxQuotaManager;
+import org.apache.james.webadmin.dto.QuotaDTO;
+import org.apache.james.webadmin.dto.QuotaRequest;
+
+public class UserQuotaService {
+
+    private final MaxQuotaManager maxQuotaManager;
+
+    @Inject
+    public UserQuotaService(MaxQuotaManager maxQuotaManager) {
+        this.maxQuotaManager = maxQuotaManager;
+    }
+
+    public void defineQuota(String user, QuotaDTO quota) throws MailboxException {
+        QuotaRoot quotaRoot = QuotaRoot.forUser(user);
+        maxQuotaManager.setMaxMessage(quotaRoot, quota.getCount());
+        maxQuotaManager.setMaxStorage(quotaRoot, quota.getSize());
+    }
+
+    public QuotaDTO getQuota(String user) throws MailboxException {
+        QuotaRoot quotaRoot = QuotaRoot.forUser(user);
+        return QuotaDTO
+            .builder()
+            .count(maxQuotaManager.getMaxMessage(quotaRoot))
+            .size(maxQuotaManager.getMaxStorage(quotaRoot))
+            .build();
+    }
+
+    public Long getMaxSizeQuota(String user) throws MailboxException {
+        return maxQuotaManager.getMaxStorage(QuotaRoot.forUser(user));
+    }
+
+    public void defineMaxSizeQuota(String user, QuotaRequest quotaRequest) throws MailboxException {
+        maxQuotaManager.setMaxStorage(QuotaRoot.forUser(user), quotaRequest.getValue());
+    }
+
+    public void deleteMaxSizeQuota(String user) throws MailboxException {
+        maxQuotaManager.setMaxStorage(QuotaRoot.forUser(user), Quota.UNLIMITED);
+    }
+
+    public Long getMaxCountQuota(String user) throws MailboxException {
+        return maxQuotaManager.getMaxMessage(QuotaRoot.forUser(user));
+    }
+
+    public void defineMaxCountQuota(String user, QuotaRequest quotaRequest) throws MailboxException {
+        maxQuotaManager.setMaxMessage(QuotaRoot.forUser(user), quotaRequest.getValue());
+    }
+
+    public void deleteMaxCountQuota(String user) throws MailboxException {
+        maxQuotaManager.setMaxMessage(QuotaRoot.forUser(user), Quota.UNLIMITED);
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/e87c0553/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserQuotaRoutesTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserQuotaRoutesTest.java b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserQuotaRoutesTest.java
new file mode 100644
index 0000000..86a09af
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/routes/UserQuotaRoutesTest.java
@@ -0,0 +1,457 @@
+/****************************************************************
+ * 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.given;
+import static com.jayway.restassured.RestAssured.when;
+import static org.apache.james.mailbox.model.Quota.UNLIMITED;
+import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+
+import org.apache.james.dnsservice.api.InMemoryDNSService;
+import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.mailbox.inmemory.quota.InMemoryPerUserMaxQuotaManager;
+import org.apache.james.mailbox.model.QuotaRoot;
+import org.apache.james.metrics.api.NoopMetricFactory;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.service.UserQuotaService;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.jayway.restassured.RestAssured;
+import com.jayway.restassured.http.ContentType;
+import com.jayway.restassured.path.json.JsonPath;
+
+public class UserQuotaRoutesTest {
+
+    private static final String QUOTA_USERS = "/quota/users";
+    private static final String PERDU_COM = "perdu.com";
+    private static final String BOB = "bob@" + PERDU_COM;
+    private static final String JOE = "joe@" + PERDU_COM;
+    private static final String PASSWORD = "secret";
+    private static final String COUNT = "count";
+    private static final String SIZE = "size";
+    private WebAdminServer webAdminServer;
+    private InMemoryPerUserMaxQuotaManager maxQuotaManager;
+    private MemoryUsersRepository usersRepository;
+
+    @Before
+    public void setUp() throws Exception {
+        maxQuotaManager = new InMemoryPerUserMaxQuotaManager();
+        MemoryDomainList memoryDomainList = new MemoryDomainList(new InMemoryDNSService());
+        memoryDomainList.setAutoDetect(false);
+        memoryDomainList.addDomain(PERDU_COM);
+        usersRepository = MemoryUsersRepository.withVirtualHosting();
+        usersRepository.setDomainList(memoryDomainList);
+        usersRepository.addUser(BOB, PASSWORD);
+        UserQuotaService userQuotaService = new UserQuotaService(maxQuotaManager);
+        UserQuotaRoutes userQuotaRoutes = new UserQuotaRoutes(usersRepository, userQuotaService, new JsonTransformer());
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+            new NoopMetricFactory(),
+            userQuotaRoutes);
+        webAdminServer.configure(NO_CONFIGURATION);
+        webAdminServer.await();
+
+        RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .build();
+        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+    }
+
+    @After
+    public void stop() {
+        webAdminServer.destroy();
+    }
+
+    @Test
+    public void getCountShouldReturnNotFoundWhenUserDoesntExist() {
+        when()
+            .get(QUOTA_USERS + "/" + JOE + "/" + COUNT)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @Test
+    public void getCountShouldReturnUnlimitedByDefault() throws UsersRepositoryException {
+        long quota =
+            given()
+                .get(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .as(Long.class);
+
+        assertThat(quota).isEqualTo(UNLIMITED);
+    }
+
+    @Test
+    public void getCountShouldReturnStoredValue() throws Exception {
+        int value = 42;
+        maxQuotaManager.setMaxMessage(QuotaRoot.forUser(BOB), value);
+
+        Long actual =
+            given()
+                .get(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .as(Long.class);
+
+        assertThat(actual).isEqualTo(value);
+    }
+
+    @Test
+    public void putCountShouldReturnNotFoundWhenUserDoesntExist() {
+        given()
+            .body("invalid")
+        .when()
+            .put(QUOTA_USERS + "/" + JOE + "/" + COUNT)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+
+    @Test
+    public void putCountShouldRejectInvalid() throws Exception {
+        Map<String, Object> errors = given()
+            .body("invalid")
+            .put(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+        .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", "Invalid quota. Need to be an integer value greater than 0")
+            .containsEntry("cause", "For input string: \"invalid\"");
+    }
+
+    @Test
+    public void putCountShouldRejectNegative() throws Exception {
+        Map<String, Object> errors = given()
+            .body("-1")
+            .put(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+        .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", "Invalid quota. Need to be an integer value greater than 0");
+    }
+
+    @Test
+    public void putCountShouldAcceptValidValue() throws Exception {
+        given()
+            .body("42")
+            .put(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxMessage(QuotaRoot.forUser(BOB))).isEqualTo(42);
+    }
+
+
+    @Test
+    @Ignore("no link between quota and mailbox for now")
+    public void putCountShouldRejectTooSmallValue() throws Exception {
+        given()
+            .body("42")
+            .put(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+            .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxMessage(QuotaRoot.forUser(BOB))).isEqualTo(42);
+    }
+
+    @Test
+    public void deleteCountShouldReturnNotFoundWhenUserDoesntExist() {
+        when()
+            .delete(QUOTA_USERS + "/" + JOE + "/" + COUNT)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+
+    @Test
+    public void deleteCountShouldSetQuotaToUnlimited() throws Exception {
+        maxQuotaManager.setMaxMessage(QuotaRoot.forUser(BOB), 42);
+
+        given()
+            .delete(QUOTA_USERS + "/" + BOB + "/" + COUNT)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxMessage(QuotaRoot.forUser(BOB))).isEqualTo(UNLIMITED);
+    }
+
+    @Test
+    public void getSizeShouldReturnNotFoundWhenUserDoesntExist() {
+            when()
+                .get(QUOTA_USERS + "/" + JOE + "/" + SIZE)
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @Test
+    public void getSizeShouldReturnUnlimitedByDefault() throws UsersRepositoryException {
+        long quota =
+            given()
+                .get(QUOTA_USERS + "/" + BOB + "/" + SIZE)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .as(Long.class);
+
+        assertThat(quota).isEqualTo(UNLIMITED);
+    }
+
+    @Test
+    public void getSizeShouldReturnStoredValue() throws Exception {
+        long value = 42;
+        maxQuotaManager.setMaxStorage(QuotaRoot.forUser(BOB), value);
+
+
+        long quota =
+            given()
+                .get(QUOTA_USERS + "/" + BOB + "/" + SIZE)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .as(Long.class);
+
+        assertThat(quota).isEqualTo(value);
+    }
+
+    @Test
+    public void putSizeShouldRejectInvalid() throws Exception {
+        Map<String, Object> errors = given()
+            .body("invalid")
+            .put(QUOTA_USERS + "/" + BOB + "/" + SIZE)
+        .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", "Invalid quota. Need to be an integer value greater than 0")
+            .containsEntry("cause", "For input string: \"invalid\"");
+    }
+
+    @Test
+    public void putSizeShouldReturnNotFoundWhenUserDoesntExist() throws Exception {
+        given()
+            .body("123")
+        .when()
+            .put(QUOTA_USERS + "/" + JOE + "/" + SIZE)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @Test
+    public void putSizeShouldRejectNegative() throws Exception {
+        Map<String, Object> errors = given()
+            .body("-1")
+            .put(QUOTA_USERS + "/" + BOB + "/" + SIZE)
+        .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", "Invalid quota. Need to be an integer value greater than 0");
+    }
+
+    @Test
+    public void putSizeShouldAcceptValidValue() throws Exception {
+        given()
+            .body("42")
+            .put(QUOTA_USERS + "/" + BOB + "/" + SIZE)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxStorage(QuotaRoot.forUser(BOB))).isEqualTo(42);
+    }
+
+    @Test
+    public void deleteSizeShouldReturnNotFoundWhenUserDoesntExist() throws Exception {
+        when()
+            .delete(QUOTA_USERS + "/" + JOE + "/" + SIZE)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @Test
+    public void deleteSizeShouldSetQuotaToUnlimited() throws Exception {
+        maxQuotaManager.setMaxStorage(QuotaRoot.forUser(BOB), 42);
+
+        given()
+            .delete(QUOTA_USERS + "/" + BOB + "/" + SIZE)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxStorage(QuotaRoot.forUser(BOB))).isEqualTo(UNLIMITED);
+    }
+
+    @Test
+    public void getQuotaShouldReturnNotFoundWhenUserDoesntExist() throws Exception {
+        when()
+            .get(QUOTA_USERS + "/" + JOE)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @Test
+    public void getQuotaShouldReturnBothWhenValueSpecified() throws Exception {
+        int maxStorage = 42;
+        int maxMessage = 52;
+        maxQuotaManager.setMaxStorage(QuotaRoot.forUser(BOB), maxStorage);
+        maxQuotaManager.setMaxMessage(QuotaRoot.forUser(BOB), maxMessage);
+
+        JsonPath jsonPath =
+            given()
+                .get(QUOTA_USERS + "/" + BOB)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .jsonPath();
+
+        assertThat(jsonPath.getLong(SIZE)).isEqualTo(maxStorage);
+        assertThat(jsonPath.getLong(COUNT)).isEqualTo(maxMessage);
+    }
+
+    @Test
+    public void getQuotaShouldReturnBothDefaultValues() throws Exception {
+        JsonPath jsonPath =
+            given()
+                .get(QUOTA_USERS + "/" + BOB)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .jsonPath();
+
+        assertThat(jsonPath.getLong(SIZE)).isEqualTo(UNLIMITED);
+        assertThat(jsonPath.getLong(COUNT)).isEqualTo(UNLIMITED);
+    }
+
+    @Test
+    public void getQuotaShouldReturnBothWhenNoCount() throws Exception {
+        int maxStorage = 42;
+        maxQuotaManager.setMaxStorage(QuotaRoot.forUser(BOB), maxStorage);
+
+        JsonPath jsonPath =
+            given()
+                .get(QUOTA_USERS + "/" + BOB)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .jsonPath();
+
+        assertThat(jsonPath.getLong(SIZE)).isEqualTo(maxStorage);
+        assertThat(jsonPath.getLong(COUNT)).isEqualTo(UNLIMITED);
+    }
+
+    @Test
+    public void getQuotaShouldReturnBothWhenNoSize() throws Exception {
+        int maxMessage = 42;
+        maxQuotaManager.setMaxMessage(QuotaRoot.forUser(BOB), maxMessage);
+
+
+        JsonPath jsonPath =
+            given()
+                .get(QUOTA_USERS + "/" + BOB)
+                .then()
+                .statusCode(HttpStatus.OK_200)
+                .contentType(ContentType.JSON)
+                .extract()
+                .jsonPath();
+
+        assertThat(jsonPath.getLong(SIZE)).isEqualTo(UNLIMITED);
+        assertThat(jsonPath.getLong(COUNT)).isEqualTo(maxMessage);
+    }
+
+    @Test
+    public void putQuotaShouldReturnNotFoundWhenUserDoesntExist() throws Exception {
+        when()
+            .put(QUOTA_USERS + "/" + JOE)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404);
+    }
+
+    @Test
+    public void putQuotaShouldUpdateBothQuota() throws Exception {
+        given()
+            .body("{\"" + COUNT + "\":52,\"" + SIZE + "\":42}")
+            .put(QUOTA_USERS + "/" + BOB)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxMessage(QuotaRoot.forUser(BOB))).isEqualTo(52);
+        assertThat(maxQuotaManager.getMaxStorage(QuotaRoot.forUser(BOB))).isEqualTo(42);
+    }
+
+    @Test
+    public void putQuotaShouldBeAbleToRemoveBothQuota() throws Exception {
+        given()
+            .body("{\"" + COUNT + "\":-1,\"" + SIZE + "\":-1}")
+            .put(QUOTA_USERS + "/" + BOB)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        assertThat(maxQuotaManager.getMaxMessage(QuotaRoot.forUser(BOB))).isEqualTo(UNLIMITED);
+        assertThat(maxQuotaManager.getMaxStorage(QuotaRoot.forUser(BOB))).isEqualTo(UNLIMITED);
+    }
+
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org