This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 8174c65554645bc6487e9996b3cd8e711dda3672 Author: Tran Tien Duc <[email protected]> AuthorDate: Thu Mar 7 11:51:35 2019 +0700 JAMES-2663 Vault route: restore by query --- .../java/org/apache/james/vault/search/Query.java | 2 +- .../integration/DeletedMessagesVaultTest.java | 6 + .../routes/DeletedMessagesVaultRestoreTask.java | 7 +- .../vault/routes/DeletedMessagesVaultRoutes.java | 47 +- .../webadmin/vault/routes/RestoreService.java | 4 +- .../webadmin/vault/routes/query/QueryElement.java | 2 +- .../vault/routes/query/QueryTranslator.java | 21 +- .../routes/DeletedMessagesVaultRoutesTest.java | 1270 +++++++++++++++++++- .../vault/routes/query/QueryTranslatorTest.java | 7 + src/site/markdown/server/manage-webadmin.md | 101 +- 10 files changed, 1402 insertions(+), 65 deletions(-) diff --git a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java index 8595a43..3092cb9 100644 --- a/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java +++ b/mailbox/plugin/deleted-messages-vault/src/main/java/org/apache/james/vault/search/Query.java @@ -30,7 +30,7 @@ public class Query { public static final Query ALL = new Query(ImmutableList.of()); private static final Predicate<DeletedMessage> MATCH_ALL = any -> true; - public static Query of(List<Criterion> criteria) { + public static Query and(List<Criterion> criteria) { return new Query(criteria); } diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java index 55476f0..4336a08 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/DeletedMessagesVaultTest.java @@ -76,6 +76,11 @@ public abstract class DeletedMessagesVaultTest { private static final ConditionFactory WAIT_TWO_MINUTES = calmlyAwait.atMost(Duration.TWO_MINUTES); private static final String SUBJECT = "This mail will be restored from the vault!!"; private static final String MAILBOX_NAME = "toBeDeleted"; + private static final String MATCH_ALL_QUERY = "{" + + "\"combinator\": \"and\"," + + "\"criteria\": []" + + "}"; + private MailboxId otherMailboxId; protected abstract GuiceJamesServer createJmapServer() throws IOException; @@ -416,6 +421,7 @@ public abstract class DeletedMessagesVaultTest { private void restoreMessagesFor(String user) { String taskId = webAdminApi.with() + .body(MATCH_ALL_QUERY) .post("/deletedMessages/user/" + user + "?action=restore") .jsonPath() .get("taskId"); diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java index b6aa3e5..9d855b8 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRestoreTask.java @@ -28,6 +28,7 @@ import org.apache.james.core.User; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.task.Task; import org.apache.james.task.TaskExecutionDetails; +import org.apache.james.vault.search.Query; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,8 +73,10 @@ class DeletedMessagesVaultRestoreTask implements Task { private final User userToRestore; private final RestoreService vaultRestore; private final AdditionalInformation additionalInformation; + private final Query query; - DeletedMessagesVaultRestoreTask(User userToRestore, RestoreService vaultRestore) { + DeletedMessagesVaultRestoreTask(RestoreService vaultRestore, User userToRestore, Query query) { + this.query = query; this.userToRestore = userToRestore; this.vaultRestore = vaultRestore; this.additionalInformation = new AdditionalInformation(userToRestore); @@ -82,7 +85,7 @@ class DeletedMessagesVaultRestoreTask implements Task { @Override public Result run() { try { - return vaultRestore.restore(userToRestore).toStream() + return vaultRestore.restore(userToRestore, query).toStream() .peek(this::updateInformation) .map(this::restoreResultToTaskResult) .reduce(Task::combine) diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java index edca31a..527b8d9 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java @@ -36,10 +36,16 @@ import org.apache.james.core.User; import org.apache.james.task.Task; import org.apache.james.task.TaskId; import org.apache.james.task.TaskManager; +import org.apache.james.vault.search.Query; import org.apache.james.webadmin.Constants; import org.apache.james.webadmin.Routes; import org.apache.james.webadmin.dto.TaskIdDto; +import org.apache.james.webadmin.utils.ErrorResponder; +import org.apache.james.webadmin.utils.JsonExtractException; +import org.apache.james.webadmin.utils.JsonExtractor; import org.apache.james.webadmin.utils.JsonTransformer; +import org.apache.james.webadmin.vault.routes.query.QueryElement; +import org.apache.james.webadmin.vault.routes.query.QueryTranslator; import org.eclipse.jetty.http.HttpStatus; import com.github.steveash.guavate.Guavate; @@ -53,6 +59,7 @@ 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; @@ -99,14 +106,18 @@ public class DeletedMessagesVaultRoutes implements Routes { private final RestoreService vaultRestore; private final JsonTransformer jsonTransformer; private final TaskManager taskManager; + private final JsonExtractor<QueryElement> jsonExtractor; + private final QueryTranslator queryTranslator; @Inject @VisibleForTesting DeletedMessagesVaultRoutes(RestoreService vaultRestore, JsonTransformer jsonTransformer, - TaskManager taskManager) { + TaskManager taskManager, QueryTranslator queryTranslator) { this.vaultRestore = vaultRestore; this.jsonTransformer = jsonTransformer; this.taskManager = taskManager; + this.queryTranslator = queryTranslator; + this.jsonExtractor = new JsonExtractor<>(QueryElement.class); } @Override @@ -145,7 +156,7 @@ public class DeletedMessagesVaultRoutes implements Routes { @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - user param is invalid"), @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.") }) - private TaskIdDto userActions(Request request, Response response) { + private TaskIdDto userActions(Request request, Response response) throws JsonExtractException { UserVaultAction requestedAction = extractUserVaultAction(request); Task requestedTask = generateTask(requestedAction, request); @@ -153,17 +164,43 @@ public class DeletedMessagesVaultRoutes implements Routes { return TaskIdDto.respond(response, taskId); } - private Task generateTask(UserVaultAction requestedAction, Request request) { - User userToRestore = User.fromUsername(request.params(USER_PATH_PARAM)); + private Task generateTask(UserVaultAction requestedAction, Request request) throws JsonExtractException { + User userToRestore = extractUser(request); + Query query = translate(jsonExtractor.parse(request.body())); switch (requestedAction) { case RESTORE: - return new DeletedMessagesVaultRestoreTask(userToRestore, vaultRestore); + return new DeletedMessagesVaultRestoreTask(vaultRestore, userToRestore, query); default: throw new NotImplementedException(requestedAction + " is not yet handled."); } } + private Query translate(QueryElement queryElement) { + try { + return queryTranslator.translate(queryElement); + } catch (QueryTranslator.QueryTranslatorException e) { + throw badRequest("Invalid payload passing to the route", e); + } + } + + private User extractUser(Request request) { + try { + return User.fromUsername(request.params(USER_PATH_PARAM)); + } catch (IllegalArgumentException e) { + throw badRequest("Invalid 'user' parameter", e); + } + } + + private HaltException badRequest(String message, Exception e) { + return ErrorResponder.builder() + .statusCode(HttpStatus.BAD_REQUEST_400) + .type(ErrorResponder.ErrorType.INVALID_ARGUMENT) + .message(message) + .cause(e) + .haltError(); + } + private UserVaultAction extractUserVaultAction(Request request) { String actionParam = request.queryParams(ACTION_QUERY_PARAM); return Optional.ofNullable(actionParam) diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java index f175c51..aa6efef 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/RestoreService.java @@ -65,11 +65,11 @@ class RestoreService { this.mailboxManager = mailboxManager; } - Flux<RestoreResult> restore(User userToRestore) throws MailboxException { + Flux<RestoreResult> restore(User userToRestore, Query searchQuery) throws MailboxException { MailboxSession session = mailboxManager.createSystemSession(userToRestore.asString()); MessageManager restoreMessageManager = restoreMailboxManager(session); - return Flux.from(deletedMessageVault.search(userToRestore, Query.ALL)) + return Flux.from(deletedMessageVault.search(userToRestore, searchQuery)) .flatMap(deletedMessage -> appendToMailbox(restoreMessageManager, deletedMessage, session)); } diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java index 2c0e9e1..1f258ba 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryElement.java @@ -22,5 +22,5 @@ package org.apache.james.webadmin.vault.routes.query; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonDeserialize(using = QueryElementDeserializer.class) -interface QueryElement { +public interface QueryElement { } diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java index 33067d6..611c2a4 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/query/QueryTranslator.java @@ -38,6 +38,7 @@ import static org.apache.james.webadmin.vault.routes.query.QueryTranslator.Opera import static org.apache.james.webadmin.vault.routes.query.QueryTranslator.Operator.EQUALS_IGNORE_CASE; import java.time.ZonedDateTime; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; @@ -205,18 +206,28 @@ public class QueryTranslator { Operator.getOperator(dto.getOperator())); } - public Query translate(QueryDTO queryDTO) throws QueryTranslatorException { - Preconditions.checkArgument(isAndCombinator(queryDTO.getCombinator()), "combinator '" + queryDTO.getCombinator() + "' is not yet handled"); + public Query translate(QueryElement queryElement) throws QueryTranslatorException { + if (queryElement instanceof QueryDTO) { + return translate((QueryDTO) queryElement); + } else if (queryElement instanceof CriterionDTO) { + return Query.of(translate((CriterionDTO) queryElement)); + } + throw new IllegalArgumentException("cannot resolve query type: " + queryElement.getClass().getName()); + } + + Query translate(QueryDTO queryDTO) throws QueryTranslatorException { + Preconditions.checkArgument(combinatorIsValid(queryDTO.getCombinator()), "combinator '" + queryDTO.getCombinator() + "' is not yet handled"); Preconditions.checkArgument(queryDTO.getCriteria().stream().allMatch(this::isCriterion), "nested query structure is not yet handled"); - return Query.of(queryDTO.getCriteria().stream() + return Query.and(queryDTO.getCriteria().stream() .map(queryElement -> (CriterionDTO) queryElement) .map(Throwing.function(this::translate)) .collect(Guavate.toImmutableList())); } - private boolean isAndCombinator(String combinator) { - return Combinator.AND.getValue().equals(combinator); + private boolean combinatorIsValid(String combinator) { + return Combinator.AND.getValue().equals(combinator) + || Objects.isNull(combinator); } private boolean isCriterion(QueryElement queryElement) { diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java index 460e3c2..1afa30d 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java @@ -21,14 +21,25 @@ package org.apache.james.webadmin.vault.routes; import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; -import static io.restassured.RestAssured.with; import static org.apache.james.vault.DeletedMessageFixture.CONTENT; import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE; import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE_2; +import static org.apache.james.vault.DeletedMessageFixture.DELETION_DATE; +import static org.apache.james.vault.DeletedMessageFixture.DELIVERY_DATE; +import static org.apache.james.vault.DeletedMessageFixture.FINAL_STAGE; +import static org.apache.james.vault.DeletedMessageFixture.MAILBOX_ID_1; +import static org.apache.james.vault.DeletedMessageFixture.MAILBOX_ID_3; +import static org.apache.james.vault.DeletedMessageFixture.SUBJECT; import static org.apache.james.vault.DeletedMessageFixture.USER; import static org.apache.james.vault.DeletedMessageFixture.USER_2; +import static org.apache.james.vault.DeletedMessageVaultSearchContract.MESSAGE_ID_GENERATOR; import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION; import static org.apache.james.webadmin.vault.routes.RestoreService.RESTORE_MAILBOX_NAME; +import static org.apache.mailet.base.MailAddressFixture.RECIPIENT1; +import static org.apache.mailet.base.MailAddressFixture.RECIPIENT2; +import static org.apache.mailet.base.MailAddressFixture.RECIPIENT3; +import static org.apache.mailet.base.MailAddressFixture.SENDER; +import static org.apache.mailet.base.MailAddressFixture.SENDER2; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -40,13 +51,17 @@ import static org.mockito.Mockito.spy; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.List; +import java.util.stream.Stream; +import org.apache.james.core.MaybeSender; import org.apache.james.core.User; import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.MessageManager; import org.apache.james.mailbox.acl.SimpleGroupMembershipResolver; import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.inmemory.InMemoryId; import org.apache.james.mailbox.inmemory.InMemoryMailboxManager; +import org.apache.james.mailbox.inmemory.InMemoryMessageId; import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources; import org.apache.james.mailbox.model.FetchGroupImpl; import org.apache.james.mailbox.model.MailboxId; @@ -57,6 +72,7 @@ import org.apache.james.mailbox.model.MultimailboxesSearchQuery; import org.apache.james.mailbox.model.SearchQuery; import org.apache.james.metrics.logger.DefaultMetricFactory; import org.apache.james.task.MemoryTaskManager; +import org.apache.james.vault.DeletedMessage; import org.apache.james.vault.memory.MemoryDeletedMessagesVault; import org.apache.james.vault.search.Query; import org.apache.james.webadmin.WebAdminServer; @@ -64,6 +80,7 @@ import org.apache.james.webadmin.WebAdminUtils; import org.apache.james.webadmin.routes.TasksRoutes; import org.apache.james.webadmin.utils.ErrorResponder; import org.apache.james.webadmin.utils.JsonTransformer; +import org.apache.james.webadmin.vault.routes.query.QueryTranslator; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -76,12 +93,19 @@ import com.google.common.collect.ImmutableList; import io.restassured.RestAssured; import io.restassured.filter.log.LogDetail; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; class DeletedMessagesVaultRoutesTest { + private static final String MATCH_ALL_QUERY = "{" + + "\"combinator\": \"and\"," + + "\"criteria\": []" + + "}"; + private WebAdminServer webAdminServer; private MemoryDeletedMessagesVault vault; private InMemoryMailboxManager mailboxManager; + private MemoryTaskManager taskManager; @BeforeEach void beforeEach() throws Exception { @@ -90,14 +114,15 @@ class DeletedMessagesVaultRoutesTest { InMemoryIntegrationResources.Resources inMemoryResource = inMemoryIntegrationResources.createResources(new SimpleGroupMembershipResolver()); mailboxManager = spy(inMemoryResource.getMailboxManager()); - MemoryTaskManager taskManager = new MemoryTaskManager(); + taskManager = new MemoryTaskManager(); JsonTransformer jsonTransformer = new JsonTransformer(); RestoreService vaultRestore = new RestoreService(vault, mailboxManager); + QueryTranslator queryTranslator = new QueryTranslator(new InMemoryId.Factory()); webAdminServer = WebAdminUtils.createWebAdminServer( new DefaultMetricFactory(), new TasksRoutes(taskManager, jsonTransformer), - new DeletedMessagesVaultRoutes(vaultRestore, jsonTransformer, taskManager)); + new DeletedMessagesVaultRoutes(vaultRestore, jsonTransformer, taskManager, queryTranslator)); webAdminServer.configure(NO_CONFIGURATION); webAdminServer.await(); @@ -110,6 +135,947 @@ class DeletedMessagesVaultRoutesTest { @AfterEach void afterEach() { webAdminServer.destroy(); + taskManager.stop(); + } + + @Nested + class QueryTest { + + @Nested + class SubjectTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingSubjectContains() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("subject contains should match") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"contains\"," + + " \"value\": \"subject contains\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntContains() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("subject") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"contains\"," + + " \"value\": \"james\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingSubjectContainsIgnoreCase() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("SUBJECT contains should match") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"containsIgnoreCase\"," + + " \"value\": \"subject contains\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntContainsIgnoreCase() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("subject") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"containsIgnoreCase\"," + + " \"value\": \"JAMES\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingSubjectEquals() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("subject should match") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"equals\"," + + " \"value\": \"subject should match\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntEquals() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("subject") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"equals\"," + + " \"value\": \"SUBJECT\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingSubjectEqualsIgnoreCase() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("SUBJECT should MatCH") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"equalsIgnoreCase\"," + + " \"value\": \"subject should match\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenSubjectDoesntEqualsIgnoreCase() throws Exception { + vault.append(USER, FINAL_STAGE.get() + .subject("subject") + .build(), new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"equalsIgnoreCase\"," + + " \"value\": \"SUBJECT Of the mail\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class DeletionDateTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingDeletionDateBeforeOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deletionDate\"," + + " \"operator\": \"beforeOrEquals\"," + + " \"value\": \"" + DELETION_DATE.plusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeletionDateBeforeOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deletionDate\"," + + " \"operator\": \"beforeOrEquals\"," + + " \"value\": \"" + DELETION_DATE.minusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingDeletionDateAfterOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deletionDate\"," + + " \"operator\": \"afterOrEquals\"," + + " \"value\": \"" + DELETION_DATE.minusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeletionDateAfterOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deletionDate\"," + + " \"operator\": \"afterOrEquals\"," + + " \"value\": \"" + DELETION_DATE.plusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class DeliveryDateTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingDeliveryDateBeforeOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deliveryDate\"," + + " \"operator\": \"beforeOrEquals\"," + + " \"value\": \"" + DELIVERY_DATE.plusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeliveryDateBeforeOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deliveryDate\"," + + " \"operator\": \"beforeOrEquals\"," + + " \"value\": \"" + DELIVERY_DATE.minusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingDeliveryDateAfterOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deliveryDate\"," + + " \"operator\": \"afterOrEquals\"," + + " \"value\": \"" + DELIVERY_DATE.minusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenNotMatchingDeliveryDateAfterOrEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"deliveryDate\"," + + " \"operator\": \"afterOrEquals\"," + + " \"value\": \"" + DELIVERY_DATE.plusHours(1).toString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class RecipientsTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingRecipientContains() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"recipients\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + RECIPIENT1.asString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenMatchingRecipientsDoNotContain() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"recipients\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + RECIPIENT3.asString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class SenderTest { + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingSenderEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"sender\"," + + " \"operator\": \"equals\"," + + " \"value\": \"" + SENDER.asString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingSenderDoesntEquals() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"sender\"," + + " \"operator\": \"equals\"," + + " \"value\": \"" + SENDER2.asString() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class HasAttachmentTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingNoAttachment() throws Exception { + DeletedMessage deletedMessage = messageWithAttachmentBuilder() + .hasAttachment(false) + .build(); + storeDeletedMessage(deletedMessage); + + String query = + "{" + + " \"fieldName\": \"hasAttachment\"," + + " \"operator\": \"equals\"," + + " \"value\": \"false\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldAppendMessageToMailboxWhenMatchingHasAttachment() throws Exception { + DeletedMessage deletedMessage = messageWithAttachmentBuilder() + .hasAttachment() + .build(); + storeDeletedMessage(deletedMessage); + + String query = + " {" + + " \"fieldName\": \"hasAttachment\"," + + " \"operator\": \"equals\"," + + " \"value\": \"true\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenMatchingHasNoAttachment() throws Exception { + DeletedMessage deletedMessage = messageWithAttachmentBuilder() + .hasAttachment(false) + .build(); + storeDeletedMessage(deletedMessage); + + String query = + "{" + + " \"fieldName\": \"hasAttachment\"," + + " \"operator\": \"equals\"," + + " \"value\": \"true\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class OriginMailboxIdsTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenContainsMailboxId() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"originMailboxes\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(1) + .hasOnlyOneElementSatisfying(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenDoNotContainsMailboxId() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"fieldName\": \"originMailboxes\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + MAILBOX_ID_3.serialize() + "\"" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } + + @Nested + class MultipleCriteriaTest { + + @Test + void restoreShouldAppendMessageToMailboxWhenAllcriteriaAreMatched() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String query = "" + + "{" + + " \"combinator\": \"and\"," + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"deliveryDate\"," + + " \"operator\": \"beforeOrEquals\"," + + " \"value\": \"" + DELIVERY_DATE.toString() + "\"" + + " }," + + " {" + + " \"fieldName\": \"recipients\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + RECIPIENT1.asString() + "\"" + + " }," + + " {" + + " \"fieldName\": \"hasAttachment\"," + + " \"operator\": \"equals\"," + + " \"value\": \"false\"" + + " }," + + " {" + + " \"fieldName\": \"originMailboxes\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" + + " }" + + " ]" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(restoreMessageContents(USER)) + .hasSize(2) + .allSatisfy(messageIS -> assertThat(messageIS).hasSameContentAs(new ByteArrayInputStream(CONTENT))); + } + + @Test + void restoreShouldNotAppendMessageToMailboxWhenASingleCriterionDoesntMatch() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); + + String query = "" + + "{" + + " \"combinator\": \"and\"," + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"deliveryDate\"," + + " \"operator\": \"beforeOrEquals\"," + + " \"value\": \"" + DELIVERY_DATE.toString() + "\"" + + " }," + + " {" + + " \"fieldName\": \"recipients\"," + + " \"operator\": \"contains\"," + + " \"value\": \"[email protected]\"" + + " }," + + " {" + + " \"fieldName\": \"hasAttachment\"," + + " \"operator\": \"equals\"," + + " \"value\": \"false\"" + + " }," + + " {" + + " \"fieldName\": \"originMailboxes\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" + + " }" + + " ]" + + "}"; + + String taskId = + given() + .queryParam("action", "restore") + .body(query) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); + + given() + .basePath(TasksRoutes.BASE) + .when() + .get(taskId + "/await") + .then() + .body("status", is("completed")); + + assertThat(hasAnyMail(USER)).isFalse(); + } + } } @Nested @@ -192,8 +1158,175 @@ class DeletedMessagesVaultRoutesTest { .then() .statusCode(HttpStatus.NOT_FOUND_404) .body("statusCode", is(404)) - .body("type", is(ErrorResponder.ErrorType.NOT_FOUND.getType())) - .body("message", is("POST /deletedMessages/user can not be found")); + .body("type", is(notNullValue())) + .body("message", is(notNullValue())); + } + + @Test + void restoreShouldReturnBadRequestWhenPassingUnsupportedField() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"unsupported\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + MAILBOX_ID_1.serialize() + "\"" + + " }" + + " ]" + + "}"; + + given() + .body(query) + .when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is(notNullValue())) + .body("details", is(notNullValue())); + } + + @Test + void restoreShouldReturnBadRequestWhenPassingUnsupportedOperator() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"subject\"," + + " \"operator\": \"isLongerThan\"," + + " \"value\": \"" + SUBJECT + "\"" + + " }" + + " ]" + + "}"; + + given() + .body(query) + .when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is(notNullValue())) + .body("details", is(notNullValue())); + } + + @Test + void restoreShouldReturnBadRequestWhenPassingUnsupportedPairOfFieldNameAndOperator() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"sender\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + SENDER.asString() + "\"" + + " }" + + " ]" + + "}"; + + given() + .body(query) + .when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is(notNullValue())) + .body("details", is(notNullValue())); + } + + @Test + void restoreShouldReturnBadRequestWhenPassingInvalidMailAddress() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"sender\"," + + " \"operator\": \"contains\"," + + " \"value\": \"invalid@[email protected]\"" + + " }" + + " ]" + + "}"; + + given() + .body(query) + .when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is(notNullValue())) + .body("details", is(notNullValue())); + } + + @Test + void restoreShouldReturnBadRequestWhenPassingOrCombinator() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"combinator\": \"or\"," + + " \"criteria\": [" + + " {" + + " \"fieldName\": \"sender\"," + + " \"operator\": \"contains\"," + + " \"value\": \"" + SENDER.asString() + "\"" + + " }" + + " ]" + + "}"; + + given() + .body(query) + .when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is(notNullValue())) + .body("details", is(notNullValue())); + } + + @Test + void restoreShouldReturnBadRequestWhenPassingNestedStructuredQuery() throws Exception { + vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); + + String query = + "{" + + " \"combinator\": \"and\"," + + " \"criteria\": [" + + " {" + + " \"combinator\": \"or\"," + + " \"criteria\": [" + + " {\"fieldName\": \"subject\", \"operator\": \"containsIgnoreCase\", \"value\": \"Apache James\"}," + + " {\"fieldName\": \"subject\", \"operator\": \"containsIgnoreCase\", \"value\": \"Apache James\"}" + + " ]" + + " }," + + " {\"fieldName\": \"subject\", \"operator\": \"containsIgnoreCase\", \"value\": \"Apache James\"}" + + " ]" + + "}"; + + given() + .body(query) + .when() + .post(USER.asString()) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is(notNullValue())) + .body("details", is(notNullValue())); } } @@ -209,11 +1342,14 @@ class DeletedMessagesVaultRoutesTest { .when(vault) .search(any(), any()); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .queryParam("action", "restore") @@ -245,11 +1381,14 @@ class DeletedMessagesVaultRoutesTest { .when(mockMessageManager) .appendMessage(any(), any()); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -275,11 +1414,14 @@ class DeletedMessagesVaultRoutesTest { .when(mailboxManager) .createMailbox(any(MailboxPath.class), any(MailboxSession.class)); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -301,6 +1443,7 @@ class DeletedMessagesVaultRoutesTest { void restoreShouldReturnATaskCreated() { given() .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) .when() .post(USER.asString()) .then() @@ -313,11 +1456,14 @@ class DeletedMessagesVaultRoutesTest { vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -340,11 +1486,14 @@ class DeletedMessagesVaultRoutesTest { vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -370,11 +1519,14 @@ class DeletedMessagesVaultRoutesTest { vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -392,11 +1544,14 @@ class DeletedMessagesVaultRoutesTest { vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -416,11 +1571,14 @@ class DeletedMessagesVaultRoutesTest { vault.append(USER, DELETED_MESSAGE, new ByteArrayInputStream(CONTENT)).block(); vault.append(USER, DELETED_MESSAGE_2, new ByteArrayInputStream(CONTENT)).block(); - String taskId = with() - .queryParam("action", "restore") - .post(USER.asString()) - .jsonPath() - .get("taskId"); + String taskId = + given() + .queryParam("action", "restore") + .body(MATCH_ALL_QUERY) + .when() + .post(USER.asString()) + .jsonPath() + .get("taskId"); given() .basePath(TasksRoutes.BASE) @@ -449,9 +1607,31 @@ class DeletedMessagesVaultRoutesTest { } } + private Stream<InputStream> restoreMessageContents(User user) throws Exception { + return restoreMailboxMessages(user).stream() + .map(this::fullContent); + } + private List<MessageResult> restoreMailboxMessages(User user) throws Exception { MailboxSession session = mailboxManager.createSystemSession(user.asString()); MessageManager messageManager = mailboxManager.getMailbox(MailboxPath.forUser(user.asString(), RESTORE_MAILBOX_NAME), session); return ImmutableList.copyOf(messageManager.getMessages(MessageRange.all(), FetchGroupImpl.MINIMAL, session)); } + + private DeletedMessage.Builder.RequireHasAttachment<DeletedMessage.Builder.FinalStage> messageWithAttachmentBuilder() { + return DeletedMessage.builder() + .messageId(InMemoryMessageId.of(MESSAGE_ID_GENERATOR.incrementAndGet())) + .originMailboxes(MAILBOX_ID_1) + .user(USER) + .deliveryDate(DELIVERY_DATE) + .deletionDate(DELETION_DATE) + .sender(MaybeSender.of(SENDER)) + .recipients(RECIPIENT1, RECIPIENT2); + } + + private DeletedMessage storeDeletedMessage(DeletedMessage deletedMessage) { + Mono.from(vault.append(USER, deletedMessage, new ByteArrayInputStream(CONTENT))) + .block(); + return deletedMessage; + } } \ No newline at end of file diff --git a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java index 6689781..f159503 100644 --- a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java +++ b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/query/QueryTranslatorTest.java @@ -47,6 +47,13 @@ class QueryTranslatorTest { } @Test + void translateShouldNotThrowWhenPassingNullOperator() { + String nullOperator = null; + assertThatCode(() -> queryTranslator.translate(new QueryDTO(nullOperator, ImmutableList.of()))) + .doesNotThrowAnyException(); + } + + @Test void translateShouldThrowWhenPassingNestedQuery() { assertThatThrownBy(() -> queryTranslator.translate(QueryDTO.and( QueryDTO.and(new CriterionDTO(FieldName.SUBJECT.getValue(), Operator.CONTAINS.getValue(), "james")) diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md index a660766..f039c15 100644 --- a/src/site/markdown/server/manage-webadmin.md +++ b/src/site/markdown/server/manage-webadmin.md @@ -2561,14 +2561,104 @@ Deleted messages of a specific user can be restored by calling the following end ``` curl -XPOST http://ip:port/deletedMessages/user/[email protected]?action=restore + +{" + "combinator": "and", + "criteria": [ + { + "fieldName": "subject", + "operator": "containsIgnoreCase", + "value": "Apache James" + }, + { + "fieldName": "deliveryDate", + "operator": "beforeOrEquals", + "value": "2014-10-30T14:12:00Z" + }, + { + "fieldName": "deletionDate", + "operator": "afterOrEquals", + "value": "2015-10-20T09:08:00Z" + }, + { + "fieldName": "recipients"," + "operator": "contains"," + "value": "[email protected]" + }, + { + "fieldName": "hasAttachment", + "operator": "equals", + "value": "false" + }, + { + "fieldName": "sender", + "operator": "equals", + "value": "[email protected]" + }, + { + "fieldName": "originMailboxes", + "operator": "contains", + "value": "02874f7c-d10e-102f-acda-0015176f7922" + } + ] +}; ``` -**All** messages in the Deleted Messages Vault of an specified user will be appended to his 'Restored-Messages' mailbox, which will be created if needed. +The requested Json body is made from list of criterion objects which have following structure: +``` +{ + "fieldName": "supportedFieldName", + "operator": "supportedOperator", + "testedValue": "plain string represents for the matching value of corresponding field" +} +``` +Deleted Messages which are matched with **all** criterions in the query body will be restored. Here are list of supported fieldName for the restoring: + - subject: represents for deleted message `subject` field matching. Supports below string operators: + - contains + - containsIgnoreCase + - equals + - equalsIgnoreCase + - deliveryDate: represents for deleted message `deliveryDate` field matching. Tested value should follow the right date time with zone offset format (ISO-8601) like + `2008-09-15T15:53:00+05:00` or `2008-09-15T15:53:00Z` + Supports below date time operators: + - beforeOrEquals: is the deleted message's `deliveryDate` before or equals the time of tested value. + - afterOrEquals: is the deleted message's `deliveryDate` after or equals the time of tested value + - deletionDate: represents for deleted message `deletionDate` field matching. Tested value & Supports operators: similar to `deliveryDate` + - sender: represents for deleted message `sender` field matching. Tested value should be a valid mail address. Supports mail address operator: + - equals: does the tested sender equal to the sender of the tested deleted message ? + - recipients: represents for deleted message `recipients` field matching. Tested value should be a valid mail address. Supports list mail address operator: + - contains: does the tested deleted message's recipients contain tested recipient ? + - hasAttachment: represents for deleted message `hasAttachment` field matching. Tested value could be `false` or `true`. Supports boolean operator: + - equals: does the tested deleted message's hasAttachment property equal to the tested hasAttachment value? + - originMailboxes: represents for deleted message `originMailboxes` field matching. Tested value is a string serialized of mailbox id. Supports list mailbox id operators: + - contains: does the tested deleted message's originMailbox ids contain tested mailbox id ? + +Messages in the Deleted Messages Vault of an specified user that are matched with Query Json Object in the body will be appended to his 'Restored-Messages' mailbox, which will be created if needed. -**Note**: - - Restoring matched messages by queries is not supported yet +**Note**: - Query parameter `action` is required and should have value `restore` to represent for restoring feature. Otherwise, a bad request response will be returned - Query parameter `action` is case sensitive + - fieldName & operator for passing to the routes are case sensitive + - Currently, we only support query combinator `and` value, otherwise, requests will be rejected + - If you only want to restore by only one criterion, the json body could be simplified to a single criterion: +``` +{ + "fieldName": "subject", + "operator": "containsIgnoreCase", + "value": "Apache James" +} +``` + - For restoring all deleted messages, passing a query json with empty criterion list to represent `matching all deleted messages`: +``` +{ + "combinator": "and", + "criteria": [] +} +``` + +**Warning**: Current web-admin uses `US` locale as the default. Therefore, there might be some conflicts when using String `containsIgnoreCase` comparators to apply +on the String data of other special locales stored in the Vault. More details at [JIRA](https://issues.apache.org/jira/browse/MAILBOX-384) + Response code: - 201: Task for restoring deleted has been created @@ -2576,13 +2666,16 @@ Response code: - action query param is not present - action query param is not a valid action - user parameter is invalid + - can not parse the JSON body + - Json query object contains unsupported operator, fieldName + - Json query object values violate parsing rules The scheduled task will have the following type `deletedMessages/restore` and the following `additionalInformation`: ``` { "successfulRestoreCount": 47, - "errorRestoreCount": 0 + "errorRestoreCount": 0, "user": "[email protected]" } ``` --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
