blob: 135a099dc93c980489040d3172595f7709b43666 [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.james.webadmin.routes;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static io.restassured.RestAssured.with;
import static io.restassured.http.ContentType.JSON;
import static javax.mail.Flags.Flag.SEEN;
import static org.apache.james.webadmin.Constants.SEPARATOR;
import static org.apache.james.webadmin.routes.UserMailboxesRoutes.USERS_BASE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400;
import static org.eclipse.jetty.http.HttpStatus.CREATED_201;
import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500;
import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404;
import static org.eclipse.jetty.http.HttpStatus.NO_CONTENT_204;
import static org.eclipse.jetty.http.HttpStatus.OK_200;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import javax.mail.Flags;
import org.apache.james.backends.opensearch.DockerElasticSearchExtension;
import org.apache.james.backends.opensearch.ElasticSearchIndexer;
import org.apache.james.backends.opensearch.ReactorElasticSearchClient;
import org.apache.james.core.Username;
import org.apache.james.json.DTOConverter;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MailboxSessionUtil;
import org.apache.james.mailbox.MessageIdManager;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.MessageUid;
import org.apache.james.mailbox.opensearch.IndexAttachments;
import org.apache.james.mailbox.opensearch.MailboxElasticSearchConstants;
import org.apache.james.mailbox.opensearch.MailboxIdRoutingKeyFactory;
import org.apache.james.mailbox.opensearch.MailboxIndexCreationUtil;
import org.apache.james.mailbox.opensearch.events.ElasticSearchListeningMessageSearchIndex;
import org.apache.james.mailbox.opensearch.json.MessageToElasticSearchJson;
import org.apache.james.mailbox.opensearch.query.CriterionConverter;
import org.apache.james.mailbox.opensearch.query.QueryConverter;
import org.apache.james.mailbox.opensearch.search.ElasticSearchSearcher;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxExistsException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.indexer.ReIndexer;
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.ByteContent;
import org.apache.james.mailbox.model.ComposedMessageId;
import org.apache.james.mailbox.model.FetchGroup;
import org.apache.james.mailbox.model.Mailbox;
import org.apache.james.mailbox.model.MailboxACL;
import org.apache.james.mailbox.model.MailboxCounters;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxMetaData;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageResult;
import org.apache.james.mailbox.model.ThreadId;
import org.apache.james.mailbox.model.UpdatedFlags;
import org.apache.james.mailbox.model.search.MailboxQuery;
import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
import org.apache.james.mailbox.store.extractor.DefaultTextExtractor;
import org.apache.james.mailbox.store.mail.model.MailboxMessage;
import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
import org.apache.james.mailbox.store.search.ListeningMessageSearchIndex;
import org.apache.james.task.Hostname;
import org.apache.james.task.MemoryTaskManager;
import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
import org.apache.james.webadmin.WebAdminServer;
import org.apache.james.webadmin.WebAdminUtils;
import org.apache.james.webadmin.dto.WebAdminUserReindexingTaskAdditionalInformationDTO;
import org.apache.james.webadmin.service.ClearMailboxContentTask;
import org.apache.james.webadmin.service.ClearMailboxContentTaskAdditionalInformationDTO;
import org.apache.james.webadmin.service.UserMailboxesService;
import org.apache.james.webadmin.utils.JsonTransformer;
import org.apache.mailbox.tools.indexer.ReIndexerImpl;
import org.apache.mailbox.tools.indexer.ReIndexerPerformer;
import org.apache.mailbox.tools.indexer.UserReindexingTask;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import io.restassured.RestAssured;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
class UserMailboxesRoutesTest {
private static final Logger LOGGER = LoggerFactory.getLogger(UserMailboxesRoutesTest.class);
public static MailboxMetaData testMetadata(MailboxPath path, MailboxId mailboxId, char delimiter) {
return new MailboxMetaData(path, mailboxId, delimiter, MailboxMetaData.Children.CHILDREN_ALLOWED_BUT_UNKNOWN, MailboxMetaData.Selectability.NONE, new MailboxACL(),
MailboxCounters.empty(mailboxId));
}
private static final Username USERNAME = Username.of("username");
private static final String MAILBOX_NAME = "myMailboxName";
private static final String MAILBOX_NAME_WITH_DOTS = "my..MailboxName";
private static final String INVALID_MAILBOX_NAME = "#myMailboxName";
private static final MailboxPath INBOX = MailboxPath.inbox(USERNAME);
private static final String ERROR_TYPE_NOTFOUND = "notFound";
private WebAdminServer webAdminServer;
private UsersRepository usersRepository;
private MemoryTaskManager taskManager;
private void createServer(MailboxManager mailboxManager, MailboxSessionMapperFactory mapperFactory, MailboxId.Factory mailboxIdFactory, ListeningMessageSearchIndex searchIndex) throws Exception {
usersRepository = mock(UsersRepository.class);
when(usersRepository.contains(USERNAME)).thenReturn(true);
taskManager = new MemoryTaskManager(new Hostname("foo"));
ReIndexerPerformer reIndexerPerformer = new ReIndexerPerformer(
mailboxManager,
searchIndex,
mapperFactory);
ReIndexer reIndexer = new ReIndexerImpl(
reIndexerPerformer,
mailboxManager,
mapperFactory);
webAdminServer = WebAdminUtils.createWebAdminServer(
new UserMailboxesRoutes(new UserMailboxesService(mailboxManager, usersRepository), new JsonTransformer(),
taskManager,
ImmutableSet.of(new UserMailboxesRoutes.UserReIndexingTaskRegistration(reIndexer))),
new TasksRoutes(taskManager, new JsonTransformer(),
DTOConverter.of(WebAdminUserReindexingTaskAdditionalInformationDTO.serializationModule(),
ClearMailboxContentTaskAdditionalInformationDTO.SERIALIZATION_MODULE)))
.start();
RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
.setBasePath(USERS_BASE + SEPARATOR + USERNAME.asString() + SEPARATOR + UserMailboxesRoutes.MAILBOXES)
.build();
}
@AfterEach
void tearDown() {
webAdminServer.destroy();
taskManager.stop();
}
@Nested
class NormalBehaviour {
private MailboxManager mailboxManager;
@BeforeEach
void setUp() throws Exception {
InMemoryMailboxManager mailboxManager = InMemoryIntegrationResources.defaultResources().getMailboxManager();
ListeningMessageSearchIndex searchIndex = mock(ListeningMessageSearchIndex.class);
Mockito.when(searchIndex.add(any(), any(), any())).thenReturn(Mono.empty());
Mockito.when(searchIndex.deleteAll(any(), any())).thenReturn(Mono.empty());
createServer(mailboxManager, mailboxManager.getMapperFactory(), new InMemoryId.Factory(), searchIndex);
this.mailboxManager = mailboxManager;
}
@Test
void getMailboxesShouldUserErrorFoundWithNonExistingUser() throws Exception {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.get()
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes");
}
@Test
void getShouldReturnNotFoundWithNonExistingUser() throws Exception {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.get(MAILBOX_NAME)
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", "User does not exist");
}
@Test
void putShouldReturnNotFoundWithNonExistingUser() throws Exception {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.put(MAILBOX_NAME)
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", "User does not exist");
}
@Test
void putShouldThrowWhenMailboxNameWithDots() throws Exception {
Map<String, Object> errors = when()
.put(MAILBOX_NAME_WITH_DOTS)
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox")
.containsEntry("details", "'#private:username:my..MailboxName' has an empty part within its mailbox name considering . as a delimiter");
}
@Test
void putShouldThrowWhenMailboxNameStartsWithDot() throws Exception {
Map<String, Object> errors = when()
.put(".startWithDot")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox")
.containsEntry("details", "'#private:username:.startWithDot' has an empty part within its mailbox name considering . as a delimiter");
}
@Test
void putShouldThrowWhenMailboxNameEndsWithDots() throws Exception {
Map<String, Object> errors = when()
.put("endWithDot.")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox")
.containsEntry("details", "'#private:username:endWithDot.' has an empty part within its mailbox name considering . as a delimiter");
}
@Test
void putShouldThrowWhenInvalidMailboxName() throws Exception {
when(usersRepository.contains(USERNAME)).thenReturn(true);
Map<String, Object> errors = when()
.put(INVALID_MAILBOX_NAME)
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox")
.containsEntry("details", "#private:username:#myMailboxName contains one of the forbidden characters %* or starts with #");
}
@Test
void deleteShouldReturnNotFoundWithNonExistingUser() throws Exception {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.put(MAILBOX_NAME)
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", "User does not exist");
}
@Test
void getShouldReturnUserErrorWithInvalidWildcardMailboxName() throws Exception {
Map<String, Object> errors = when()
.get(MAILBOX_NAME + "*")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to test existence of an invalid mailbox")
.containsEntry("details", "#private:username:myMailboxName* contains one of the forbidden characters %* or starts with #");
}
@Test
void putShouldReturnUserErrorWithInvalidWildcardMailboxName() throws Exception {
Map<String, Object> errors = when()
.put(MAILBOX_NAME + "*")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox");
}
@Test
void deleteShouldReturnUserErrorWithInvalidWildcardMailboxName() throws Exception {
Map<String, Object> errors = when()
.put(MAILBOX_NAME + "*")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox");
}
@Test
void getShouldReturnUserErrorWithInvalidPercentMailboxName() throws Exception {
Map<String, Object> errors = when()
.get(MAILBOX_NAME + "%")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to test existence of an invalid mailbox")
.containsEntry("details", "#private:username:myMailboxName% contains one of the forbidden characters %* or starts with #");
}
@Test
void putShouldReturnUserErrorWithInvalidPercentMailboxName() throws Exception {
Map<String, Object> errors = when()
.put(MAILBOX_NAME + "%")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox");
}
@Test
void deleteShouldReturnUserErrorWithInvalidPercentMailboxName() throws Exception {
Map<String, Object> errors = when()
.put(MAILBOX_NAME + "%")
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox");
}
@Test
void getShouldReturnUserErrorWithInvalidSharpMailboxName() throws Exception {
Map<String, Object> errors = when()
.get("#" + MAILBOX_NAME)
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to test existence of an invalid mailbox")
.containsEntry("details", "#private:username:#myMailboxName contains one of the forbidden characters %* or starts with #");
}
@Test
void getShouldReturnOkWhenSharpInTheMiddleOfTheName() throws Exception {
with()
.put("a#b");
when()
.get("a#b")
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void putShouldReturnUserErrorWithInvalidSharpMailboxName() throws Exception {
Map<String, Object> errors = when()
.put("#" + MAILBOX_NAME)
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox");
}
@Test
void putShouldAcceptMailboxNamesContainingSharp() throws Exception {
when()
.put("a#b")
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldReturnUserErrorWithInvalidSharpMailboxName() throws Exception {
Map<String, Object> errors = when()
.put("#" + MAILBOX_NAME)
.then()
.statusCode(BAD_REQUEST_400)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", BAD_REQUEST_400)
.containsEntry("type", "InvalidArgument")
.containsEntry("message", "Attempt to create an invalid mailbox");
}
@Test
void deleteShouldAcceptSharpInTheMiddleOfTheName() throws Exception {
when()
.put("a#b")
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void getShouldReturnNotFoundWithAndMailboxName() throws Exception {
when()
.get(MAILBOX_NAME + "&")
.then()
.statusCode(NOT_FOUND_404);
}
@Test
void putShouldReturnSuccessWithAndMailboxName() throws Exception {
when()
.put(MAILBOX_NAME + "&")
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldReturnSuccessWithAndMailboxName() throws Exception {
when()
.put(MAILBOX_NAME + "&")
.then()
.statusCode(NO_CONTENT_204)
.contentType(JSON);
}
@Test
void deleteMailboxesShouldReturnUserErrorWithNonExistingUser() throws Exception {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.delete()
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid delete on user mailboxes");
}
@Test
void getMailboxesShouldReturnEmptyListByDefault() {
List<Object> list =
when()
.get()
.then()
.statusCode(OK_200)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getList(".");
assertThat(list).isEmpty();
}
@Test
void putShouldReturnNotFoundWhenNoMailboxName() {
when()
.put()
.then()
.statusCode(NOT_FOUND_404);
}
@Test
void putShouldReturnNotFoundWhenJustSeparator() {
when()
.put(SEPARATOR)
.then()
.statusCode(NOT_FOUND_404);
}
@Test
void putShouldReturnOk() {
when()
.put(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void putShouldReturnOkWhenIssuedTwoTimes() {
with()
.put(MAILBOX_NAME);
when()
.put(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void putShouldAddAMailbox() {
with()
.put(MAILBOX_NAME);
when()
.get()
.then()
.statusCode(OK_200)
.body(".", hasSize(1))
.body("[0].mailboxName", is("myMailboxName"))
.body("[0].mailboxId", is("1"));
}
@Test
void getShouldReturnNotFoundWhenMailboxDoesNotExist() {
Map<String, Object> errors = when()
.get(MAILBOX_NAME)
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Mailbox does not exist");
}
@Test
void getShouldReturnOkWhenMailboxExists() {
with()
.put(MAILBOX_NAME);
when()
.get(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldReturnOkWhenMailboxDoesNotExist() {
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldReturnOkWhenMailboxExists() {
with()
.put(MAILBOX_NAME);
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldRemoveMailbox() {
with()
.put(MAILBOX_NAME);
with()
.delete(MAILBOX_NAME);
Map<String, Object> errors = when()
.get(MAILBOX_NAME)
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Mailbox does not exist");
}
@Test
void deleteMailboxesShouldReturnOkWhenNoMailboxes() {
when()
.delete()
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteMailboxesShouldReturnOkWhenMailboxes() {
with()
.put(MAILBOX_NAME);
when()
.delete()
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteMailboxesShouldRemoveAllUserMailboxes() {
with()
.put(MAILBOX_NAME);
with()
.put("otherMailbox");
with()
.delete();
List<Object> list =
when()
.get()
.then()
.statusCode(OK_200)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getList(".");
assertThat(list).isEmpty();
}
@Test
void deleteShouldReturnOkWhenMailboxHasChildren() {
with()
.put(MAILBOX_NAME);
with()
.put(MAILBOX_NAME + ".child");
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldDeleteAMailboxAndItsChildren() {
with()
.put(MAILBOX_NAME);
with()
.put(MAILBOX_NAME + ".child");
with()
.delete(MAILBOX_NAME);
List<Object> list =
when()
.get()
.then()
.statusCode(OK_200)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getList(".");
assertThat(list).isEmpty();
}
@Test
void deleteShouldNotDeleteUnrelatedMailbox() {
String mailboxName = MAILBOX_NAME + "!child";
with()
.put(MAILBOX_NAME);
with()
.put(mailboxName);
with()
.delete(MAILBOX_NAME);
List<Map<String, String>> list =
when()
.get()
.then()
.statusCode(OK_200)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getList(".");
assertThat(list)
.hasSize(1)
.first()
.satisfies(map -> assertThat(map).hasSize(2)
.containsKeys("mailboxId")
.containsEntry("mailboxName", mailboxName));
}
@Test
void deleteShouldReturnOkWhenDeletingChildMailboxes() {
with()
.put(MAILBOX_NAME);
with()
.put(MAILBOX_NAME + ".child");
when()
.delete(MAILBOX_NAME + ".child")
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldBeAbleToRemoveChildMailboxes() {
with()
.put(MAILBOX_NAME);
with()
.put(MAILBOX_NAME + ".child");
with()
.delete(MAILBOX_NAME + ".child");
List<Map<String, String>> list =
when()
.get()
.then()
.statusCode(OK_200)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getList(".");
assertThat(list)
.hasSize(1)
.first()
.satisfies(map -> assertThat(map).hasSize(2)
.containsKeys("mailboxId")
.containsEntry("mailboxName", MAILBOX_NAME));
}
@Test
void getMessageCountShouldReturnZeroWhenMailBoxEmpty() {
with()
.put(MAILBOX_NAME);
String response = when()
.get(MAILBOX_NAME + "/messageCount")
.then()
.statusCode(OK_200)
.extract()
.body().asString();
assertThat(response)
.isEqualTo("0");
}
@Test
void getMessageCountShouldReturnTotalEmailsInMailBox() {
with()
.put(MAILBOX_NAME);
MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
IntStream.range(0, 10)
.forEach(index -> {
try {
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession);
} catch (MailboxException e) {
LOGGER.warn("Error when append message " + e);
}
});
String response = when()
.get(MAILBOX_NAME + "/messageCount")
.then()
.statusCode(OK_200)
.extract()
.body().asString();
assertThat(response)
.isEqualTo("10");
}
@Test
void getMessageCountShouldReturnErrorWhenUserIsNotFound() throws UsersRepositoryException {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.get(MAILBOX_NAME + "/messageCount")
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", "User does not exist");
}
@Test
void getMessageCountShouldReturnErrorWhenMailboxDoesNotExist() {
Map<String, Object> errors = when()
.get(MAILBOX_NAME + "/messageCount")
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", String.format("#private:%s:%s can not be found", USERNAME.asString(), MAILBOX_NAME));
}
@Test
void getUnseenMessageCountShouldReturnZeroWhenMailBoxEmpty() {
with()
.put(MAILBOX_NAME);
String response = when()
.get(MAILBOX_NAME + "/unseenMessageCount")
.then()
.statusCode(OK_200)
.extract()
.body().asString();
assertThat(response)
.isEqualTo("0");
}
@Test
void getUnseenMessageCountShouldReturnZeroWhenMailBoxDoNotHaveAnyUnSeenEmail() {
with()
.put(MAILBOX_NAME);
MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
IntStream.range(0, 10)
.forEach(index -> {
try {
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder()
.withFlags(new Flags(SEEN))
.build("header: value\r\n\r\nbody"),
systemSession);
} catch (MailboxException e) {
LOGGER.warn("Error when append message " + e);
}
});
String response = when()
.get(MAILBOX_NAME + "/unseenMessageCount")
.then()
.statusCode(OK_200)
.extract()
.body().asString();
assertThat(response)
.isEqualTo("0");
}
@Test
void getUnseenMessageCountShouldReturnNumberOfUnSeenEmails() {
with()
.put(MAILBOX_NAME);
MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
IntStream.range(0, 5)
.forEach(index -> {
try {
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder()
.withFlags(new Flags(SEEN))
.build("header: value\r\n\r\nbody"),
systemSession);
} catch (MailboxException e) {
LOGGER.warn("Error when append message " + e);
}
});
IntStream.range(0, 10)
.forEach(index -> {
try {
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder()
.build("header: value\r\n\r\nbody"),
systemSession);
} catch (MailboxException e) {
LOGGER.warn("Error when append message " + e);
}
});
String response = when()
.get(MAILBOX_NAME + "/unseenMessageCount")
.then()
.statusCode(OK_200)
.extract()
.body().asString();
assertThat(response)
.isEqualTo("10");
}
@Test
void getUnseenMessageCountShouldReturnErrorWhenUserIsNotFound() throws UsersRepositoryException {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.get(MAILBOX_NAME + "/unseenMessageCount")
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", "User does not exist");
}
@Test
void getUnseenMessageCountShouldReturnErrorWhenMailboxDoesNotExist() {
Map<String, Object> errors = when()
.get(MAILBOX_NAME + "/unseenMessageCount")
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", String.format("#private:%s:%s can not be found", USERNAME.asString(), MAILBOX_NAME));
}
@Test
void deleteMailboxContentShouldReturnErrorWhenUserIsNotFound() throws UsersRepositoryException {
when(usersRepository.contains(USERNAME)).thenReturn(false);
Map<String, Object> errors = when()
.delete(MAILBOX_NAME + "/messages")
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", "User does not exist");
}
@Test
void deleteMailboxContentShouldReturnErrorWhenMailboxDoesNotExist() {
Map<String, Object> errors = when()
.delete(MAILBOX_NAME + "/messages")
.then()
.statusCode(NOT_FOUND_404)
.contentType(JSON)
.extract()
.body()
.jsonPath()
.getMap(".");
assertThat(errors)
.containsEntry("statusCode", NOT_FOUND_404)
.containsEntry("type", ERROR_TYPE_NOTFOUND)
.containsEntry("message", "Invalid get on user mailboxes")
.containsEntry("details", String.format("Mailbox does not exist. #private:%s:%s", USERNAME.asString(), MAILBOX_NAME));
}
@Test
void deleteMailboxContentShouldReturnTaskId() {
with()
.put(MAILBOX_NAME);
String taskId = when()
.delete(MAILBOX_NAME + "/messages")
.then()
.statusCode(CREATED_201)
.extract()
.jsonPath()
.get("taskId");
assertThat(taskId)
.isNotEmpty();
}
}
@Nested
class ExceptionHandling {
private MailboxManager mailboxManager;
@BeforeEach
void setUp() throws Exception {
mailboxManager = mock(MailboxManager.class);
when(mailboxManager.createSystemSession(any())).thenReturn(MailboxSessionUtil.create(USERNAME));
ListeningMessageSearchIndex searchIndex = mock(ListeningMessageSearchIndex.class);
Mockito.when(searchIndex.add(any(), any(), any())).thenReturn(Mono.empty());
Mockito.when(searchIndex.deleteAll(any(), any())).thenReturn(Mono.empty());
createServer(mailboxManager, mock(MailboxSessionMapperFactory.class), new InMemoryId.Factory(), searchIndex);
}
@Test
void putShouldGenerateInternalErrorOnUnknownException() throws Exception {
doThrow(new RuntimeException()).when(mailboxManager).createMailbox(any(), any());
when()
.put(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void putShouldGenerateInternalErrorOnUnknownMailboxException() throws Exception {
doThrow(new MailboxException()).when(mailboxManager).createMailbox(any(), any());
when()
.put(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void putShouldReturnOkOnMailboxExists() throws Exception {
doThrow(new MailboxExistsException(MAILBOX_NAME)).when(mailboxManager).createMailbox(any(), any());
when()
.put(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldGenerateInternalErrorOnUnknownExceptionOnDelete() throws Exception {
MailboxId mailboxId = InMemoryId.of(12);
when(mailboxManager.search(any(MailboxQuery.class), any()))
.thenReturn(
Flux.just(
testMetadata(
MailboxPath.forUser(USERNAME, MAILBOX_NAME), mailboxId, '.')));
doThrow(new RuntimeException()).when(mailboxManager).deleteMailbox(any(MailboxPath.class), any());
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldGenerateInternalErrorOnUnknownExceptionOnSearch() throws Exception {
when(mailboxManager.search(any(MailboxQuery.class), any())).thenReturn(Flux.error(new RuntimeException()));
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldGenerateInternalErrorOnUnknownMailboxExceptionOnDelete() throws Exception {
MailboxId mailboxId = InMemoryId.of(12);
when(mailboxManager.search(any(MailboxQuery.class), any()))
.thenReturn(
Flux.just(
testMetadata(MailboxPath.forUser(USERNAME, MAILBOX_NAME), mailboxId, '.')));
doThrow(new MailboxException()).when(mailboxManager).deleteMailbox(any(MailboxPath.class), any());
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldGenerateInternalErrorOnUnknownMailboxExceptionOnSearch() throws Exception {
when(mailboxManager.search(any(MailboxQuery.class), any())).thenReturn(Flux.error(new MailboxException()));
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldReturnOkOnMailboxDoesNotExists() throws Exception {
MailboxMetaData metaData = mock(MailboxMetaData.class);
when(metaData.getPath()).thenReturn(MailboxPath.forUser(USERNAME, MAILBOX_NAME));
doReturn(Flux.just(metaData))
.when(mailboxManager).search(any(MailboxQuery.class), any(), any(MailboxSession.class));
doThrow(new MailboxNotFoundException(MAILBOX_NAME)).when(mailboxManager).deleteMailbox(any(MailboxPath.class), any());
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldGenerateInternalErrorOnUnknownExceptionWhenListingMailboxes() throws Exception {
when(mailboxManager.search(any(MailboxQuery.class), any())).thenReturn(Flux.error(new RuntimeException()));
when()
.delete()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldGenerateInternalErrorOnMailboxExceptionWhenListingMailboxes() throws Exception {
when(mailboxManager.search(any(MailboxQuery.class), any())).thenReturn(Flux.error(new MailboxException()));
when()
.delete()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldGenerateInternalErrorOnUnknownExceptionWhenRemovingMailboxes() throws Exception {
MailboxId mailboxId = InMemoryId.of(12);
when(mailboxManager.search(any(MailboxQuery.class), any()))
.thenReturn(
Flux.just(
testMetadata(MailboxPath.forUser(USERNAME, "any"), mailboxId, '.')));
doThrow(new RuntimeException()).when(mailboxManager).deleteMailbox(any(MailboxPath.class), any());
when()
.delete()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldReturnOkOnMailboxNotFoundExceptionWhenRemovingMailboxes() throws Exception {
MailboxId mailboxId = InMemoryId.of(12);
when(mailboxManager.search(any(MailboxQuery.class), any(), any()))
.thenReturn(
Flux.just(testMetadata(MailboxPath.forUser(USERNAME, "any"), mailboxId, '.')));
doThrow(new MailboxNotFoundException("any")).when(mailboxManager).deleteMailbox(any(MailboxPath.class), any());
when()
.delete()
.then()
.statusCode(NO_CONTENT_204);
}
@Test
void deleteShouldReturnInternalErrorOnMailboxExceptionWhenRemovingMailboxes() throws Exception {
MailboxId mailboxId = InMemoryId.of(12);
when(mailboxManager.search(any(MailboxQuery.class), any()))
.thenReturn(
Flux.just(testMetadata(MailboxPath.forUser(USERNAME, "any"), mailboxId, '.')));
doThrow(new MailboxException()).when(mailboxManager).deleteMailbox(any(MailboxPath.class), any());
when()
.delete()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void getShouldGenerateInternalErrorOnUnknownException() throws Exception {
doReturn(Mono.error(new RuntimeException())).when(mailboxManager).mailboxExists(any(), any());
when()
.get(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void getShouldGenerateInternalErrorOnUnknownMailboxException() throws Exception {
doReturn(Mono.error(new MailboxException())).when(mailboxManager).mailboxExists(any(), any());
when()
.get(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void getMailboxesShouldGenerateInternalErrorOnUnknownException() throws Exception {
when(mailboxManager.search(any(MailboxQuery.class), any())).thenReturn(Flux.error(new RuntimeException()));
when()
.get()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void getMailboxesShouldGenerateInternalErrorOnUnknownMailboxException() throws Exception {
when(mailboxManager.search(any(MailboxQuery.class), any())).thenReturn(Flux.error(new MailboxException()));
when()
.get()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void getMailboxesShouldGenerateInternalErrorOnRepositoryException() throws Exception {
doThrow(new RuntimeException()).when(usersRepository).contains(USERNAME);
when()
.get()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void getShouldGenerateInternalErrorOnRepositoryException() throws Exception {
doThrow(new RuntimeException()).when(usersRepository).contains(USERNAME);
when()
.get(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void putShouldGenerateInternalErrorOnRepositoryException() throws Exception {
doThrow(new RuntimeException()).when(usersRepository).contains(USERNAME);
when()
.put(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteShouldGenerateInternalErrorOnRepositoryException() throws Exception {
doThrow(new RuntimeException()).when(usersRepository).contains(USERNAME);
when()
.delete(MAILBOX_NAME)
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
@Test
void deleteMailboxesShouldGenerateInternalErrorOnRepositoryException() throws Exception {
doThrow(new RuntimeException()).when(usersRepository).contains(USERNAME);
when()
.delete()
.then()
.statusCode(INTERNAL_SERVER_ERROR_500);
}
}
@Nested
class UserReIndexing {
static final int SEARCH_SIZE = 1;
@RegisterExtension
DockerElasticSearchExtension elasticSearch = new DockerElasticSearchExtension();
private InMemoryMailboxManager mailboxManager;
private ListeningMessageSearchIndex searchIndex;
MessageIdManager messageIdManager;
@BeforeEach
void setUp() throws Exception {
ReactorElasticSearchClient client = MailboxIndexCreationUtil.prepareDefaultClient(
elasticSearch.getDockerElasticSearch().clientProvider().get(),
elasticSearch.getDockerElasticSearch().configuration());
InMemoryMessageId.Factory messageIdFactory = new InMemoryMessageId.Factory();
MailboxIdRoutingKeyFactory routingKeyFactory = new MailboxIdRoutingKeyFactory();
InMemoryIntegrationResources resources = InMemoryIntegrationResources.builder()
.preProvisionnedFakeAuthenticator()
.fakeAuthorizator()
.inVmEventBus()
.defaultAnnotationLimits()
.defaultMessageParser()
.listeningSearchIndex(preInstanciationStage -> new ElasticSearchListeningMessageSearchIndex(
preInstanciationStage.getMapperFactory(),
ImmutableSet.of(),
new ElasticSearchIndexer(client,
MailboxElasticSearchConstants.DEFAULT_MAILBOX_WRITE_ALIAS),
new ElasticSearchSearcher(client, new QueryConverter(new CriterionConverter()), SEARCH_SIZE,
MailboxElasticSearchConstants.DEFAULT_MAILBOX_READ_ALIAS, routingKeyFactory),
new MessageToElasticSearchJson(new DefaultTextExtractor(), ZoneId.of("Europe/Paris"), IndexAttachments.YES),
preInstanciationStage.getSessionProvider(), routingKeyFactory, messageIdFactory))
.noPreDeletionHooks()
.storeQuotaManager()
.build();
mailboxManager = resources.getMailboxManager();
messageIdManager = resources.getMessageIdManager();
searchIndex = spy((ListeningMessageSearchIndex) resources.getSearchIndex());
createServer(mailboxManager, mailboxManager.getMapperFactory(), new InMemoryId.Factory(), searchIndex);
}
@Nested
class Validation {
@Test
void userReprocessingShouldFailWithNoTask() {
when()
.post()
.then()
.statusCode(BAD_REQUEST_400)
.body("statusCode", Matchers.is(400))
.body("type", Matchers.is("InvalidArgument"))
.body("message", Matchers.is("Invalid arguments supplied in the user request"))
.body("details", Matchers.is("'task' query parameter is compulsory. Supported values are [reIndex]"));
}
@Test
void userReprocessingShouldFailWithBadTask() {
given()
.queryParam("task", "bad")
.when()
.post()
.then()
.statusCode(BAD_REQUEST_400)
.body("statusCode", Matchers.is(400))
.body("type", Matchers.is("InvalidArgument"))
.body("message", Matchers.is("Invalid arguments supplied in the user request"))
.body("details", Matchers.is("Invalid value supplied for query parameter 'task': bad. Supported values are [reIndex]"));
}
@Test
void userReprocessingShouldFailWithBadUser() {
RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
.setBasePath(USERS_BASE + SEPARATOR + "bad@bad@bad" + SEPARATOR + UserMailboxesRoutes.MAILBOXES)
.build();
given()
.queryParam("user", "bad@bad@bad")
.queryParam("task", "reIndex")
.when()
.post()
.then()
.statusCode(BAD_REQUEST_400)
.body("statusCode", Matchers.is(400))
.body("type", Matchers.is("InvalidArgument"))
.body("message", Matchers.is("Invalid arguments supplied in the user request"))
.body("details", Matchers.is("Domain parts ASCII chars must be a-z A-Z 0-9 - or _"));
}
}
@Nested
class TaskDetails {
@Test
void userReprocessingShouldNotFailWhenNoMail() {
String taskId = given()
.queryParam("task", "reIndex")
.when()
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(UserReindexingTask.USER_RE_INDEXING.asString()))
.body("additionalInformation.username", Matchers.is("username"))
.body("additionalInformation.successfullyReprocessedMailCount", Matchers.is(0))
.body("additionalInformation.failedReprocessedMailCount", Matchers.is(0))
.body("additionalInformation.runningOptions.messagesPerSecond", Matchers.is(50))
.body("additionalInformation.runningOptions.mode", Matchers.is("REBUILD_ALL"))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()))
.body("completedDate", Matchers.is(notNullValue()));
}
@Test
void userReprocessingShouldReturnTaskDetailsWhenMail() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
mailboxManager.createMailbox(INBOX, systemSession).get();
mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession);
String taskId = given()
.queryParam("task", "reIndex")
.when()
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(UserReindexingTask.USER_RE_INDEXING.asString()))
.body("additionalInformation.username", Matchers.is("username"))
.body("additionalInformation.successfullyReprocessedMailCount", Matchers.is(1))
.body("additionalInformation.failedReprocessedMailCount", Matchers.is(0))
.body("additionalInformation.runningOptions.messagesPerSecond", Matchers.is(50))
.body("additionalInformation.runningOptions.mode", Matchers.is("REBUILD_ALL"))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()))
.body("completedDate", Matchers.is(notNullValue()));
}
@Test
void userReprocessingWithMessagesPerSecondShouldReturnTaskDetailsWhenMail() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
mailboxManager.createMailbox(INBOX, systemSession).get();
mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession);
String taskId = given()
.queryParam("task", "reIndex")
.queryParam("messagesPerSecond", 1)
.when()
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(UserReindexingTask.USER_RE_INDEXING.asString()))
.body("additionalInformation.username", Matchers.is("username"))
.body("additionalInformation.successfullyReprocessedMailCount", Matchers.is(1))
.body("additionalInformation.failedReprocessedMailCount", Matchers.is(0))
.body("additionalInformation.runningOptions.messagesPerSecond", Matchers.is(1))
.body("additionalInformation.runningOptions.mode", Matchers.is("REBUILD_ALL"))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()))
.body("completedDate", Matchers.is(notNullValue()));
}
@Test
void userReprocessingShouldReturnTaskDetailsWhenFailing() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
ComposedMessageId composedMessageId = mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession).getId();
doReturn(Mono.error(new RuntimeException()))
.when(searchIndex)
.add(any(MailboxSession.class), any(Mailbox.class), any(MailboxMessage.class));
String taskId = with()
.queryParam("task", "reIndex")
.post()
.jsonPath()
.get("taskId");
long uidAsLong = composedMessageId.getUid().asLong();
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("failed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(UserReindexingTask.USER_RE_INDEXING.asString()))
.body("additionalInformation.successfullyReprocessedMailCount", Matchers.is(0))
.body("additionalInformation.failedReprocessedMailCount", Matchers.is(1))
.body("additionalInformation.runningOptions.messagesPerSecond", Matchers.is(50))
.body("additionalInformation.runningOptions.mode", Matchers.is("REBUILD_ALL"))
.body("additionalInformation.messageFailures.\"" + mailboxId.serialize() + "\"[0].uid", Matchers.is(Long.valueOf(uidAsLong).intValue()))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()));
}
@Test
void userReprocessingShouldReturnTaskDetailsWhenFailingAtTheMailboxLevel() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
doReturn(Mono.error(new RuntimeException()))
.when(searchIndex)
.deleteAll(any(MailboxSession.class), any(MailboxId.class));
String taskId = with()
.queryParam("task", "reIndex")
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("failed"))
.body("taskId", Matchers.is(notNullValue()))
.body("additionalInformation.mailboxFailures", Matchers.containsInAnyOrder(mailboxId.serialize()));
}
@Test
void userReprocessingWithCorrectModeShouldReturnTaskDetailsWhenMails() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
Mailbox mailbox = mailboxManager.getMailbox(mailboxId, systemSession).getMailboxEntity();
ComposedMessageId result = mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession)
.getId();
mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession);
List<MessageResult> messages = messageIdManager.getMessages(ImmutableList.of(result.getMessageId()), FetchGroup.MINIMAL, systemSession);
Flags newFlags = new Flags(Flags.Flag.DRAFT);
UpdatedFlags updatedFlags = UpdatedFlags.builder()
.uid(result.getUid())
.modSeq(messages.get(0).getModSeq())
.oldFlags(new Flags())
.newFlags(newFlags)
.build();
// We update on the searchIndex level to try to create inconsistencies
searchIndex.update(systemSession, mailbox.getMailboxId(), ImmutableList.of(updatedFlags)).block();
String taskId = with()
.queryParam("task", "reIndex")
.queryParam("mode", "fixOutdated")
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(UserReindexingTask.USER_RE_INDEXING.asString()))
.body("additionalInformation.successfullyReprocessedMailCount", Matchers.is(2))
.body("additionalInformation.failedReprocessedMailCount", Matchers.is(0))
.body("additionalInformation.runningOptions.messagesPerSecond", Matchers.is(50))
.body("additionalInformation.runningOptions.mode", Matchers.is("FIX_OUTDATED"))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()))
.body("completedDate", Matchers.is(notNullValue()));
}
@Test
void userReprocessingWithCorrectModeShouldFixInconsistenciesInES() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
Mailbox mailbox = mailboxManager.getMailbox(mailboxId, systemSession).getMailboxEntity();
ComposedMessageId result = mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession)
.getId();
Flags initialFlags = searchIndex.retrieveIndexedFlags(mailbox, result.getUid()).block();
List<MessageResult> messages = messageIdManager.getMessages(ImmutableList.of(result.getMessageId()), FetchGroup.MINIMAL, systemSession);
Flags newFlags = new Flags(Flags.Flag.DRAFT);
UpdatedFlags updatedFlags = UpdatedFlags.builder()
.uid(result.getUid())
.modSeq(messages.get(0).getModSeq())
.oldFlags(new Flags())
.newFlags(newFlags)
.build();
// We update on the searchIndex level to try to create inconsistencies
searchIndex.update(systemSession, mailbox.getMailboxId(), ImmutableList.of(updatedFlags)).block();
String taskId = with()
.queryParam("task", "reIndex")
.queryParam("mode", "fixOutdated")
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await");
assertThat(searchIndex.retrieveIndexedFlags(mailbox, result.getUid()).block())
.isEqualTo(initialFlags);
}
@Test
void userReprocessingWithCorrectModeShouldNotChangeDocumentsInESWhenNoInconsistencies() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
Mailbox mailbox = mailboxManager.getMailbox(mailboxId, systemSession).getMailboxEntity();
ComposedMessageId result = mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession)
.getId();
Flags initialFlags = searchIndex.retrieveIndexedFlags(mailbox, result.getUid()).block();
String taskId = with()
.queryParam("task", "reIndex")
.queryParam("mode", "fixOutdated")
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await");
assertThat(searchIndex.retrieveIndexedFlags(mailbox, result.getUid()).block())
.isEqualTo(initialFlags);
}
@Disabled("JAMES-3202 Limitation of the current correct mode reindexation. We only check metadata and fix "
+ "inconsistencies with ES, but we don't check for inconsistencies from ES to metadata")
@Test
void userReprocessingWithCorrectModeShouldRemoveOrphanMessagesInES() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
Mailbox mailbox = mailboxManager.getMailbox(mailboxId, systemSession).getMailboxEntity();
byte[] content = "Simple message content".getBytes(StandardCharsets.UTF_8);
MessageUid uid = MessageUid.of(22L);
SimpleMailboxMessage message = SimpleMailboxMessage.builder()
.messageId(InMemoryMessageId.of(42L))
.threadId(ThreadId.fromBaseMessageId(InMemoryMessageId.of(42L)))
.uid(uid)
.content(new ByteContent(content))
.size(content.length)
.internalDate(new Date(ZonedDateTime.parse("2018-02-15T15:54:02Z").toEpochSecond()))
.bodyStartOctet(0)
.flags(new Flags("myFlags"))
.properties(new PropertyBuilder())
.mailboxId(mailboxId)
.build();
searchIndex.add(systemSession, mailbox, message).block();
String taskId = with()
.queryParam("task", "reIndex")
.queryParam("mode", "fixOutdated")
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await");
assertThat(searchIndex.retrieveIndexedFlags(mailbox, uid).blockOptional())
.isEmpty();
}
@Test
void userReprocessingShouldReturnTaskDetailWhenDeleteMailboxContentWithNoEmail() {
with()
.put(MAILBOX_NAME);
String taskId = when()
.delete(MAILBOX_NAME + "/messages")
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(ClearMailboxContentTask.TASK_TYPE.asString()))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()))
.body("completedDate", Matchers.is(notNullValue()))
.body("additionalInformation.username", Matchers.is(USERNAME.asString()))
.body("additionalInformation.mailboxName", Matchers.is(MAILBOX_NAME))
.body("additionalInformation.messagesSuccessCount", Matchers.is(0))
.body("additionalInformation.messagesFailCount", Matchers.is(0));
}
@Test
void userReprocessingShouldReturnTaskDetailWhenDeleteMailboxContentWithHasEmails() {
with()
.put(MAILBOX_NAME);
MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
IntStream.range(0, 10)
.forEach(index -> {
try {
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder()
.build("header: value\r\n\r\nbody"),
systemSession);
} catch (MailboxException e) {
LOGGER.warn("Error when append message " + e);
}
});
String taskId = when()
.delete(MAILBOX_NAME + "/messages")
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"))
.body("taskId", Matchers.is(notNullValue()))
.body("type", Matchers.is(ClearMailboxContentTask.TASK_TYPE.asString()))
.body("startedDate", Matchers.is(notNullValue()))
.body("submitDate", Matchers.is(notNullValue()))
.body("completedDate", Matchers.is(notNullValue()))
.body("additionalInformation.username", Matchers.is(USERNAME.asString()))
.body("additionalInformation.mailboxName", Matchers.is(MAILBOX_NAME))
.body("additionalInformation.messagesSuccessCount", Matchers.is(10))
.body("additionalInformation.messagesFailCount", Matchers.is(0));
}
@Test
void userReprocessingShouldEmptyMailboxWhenDeleteMailboxContent() throws MailboxException {
with()
.put(MAILBOX_NAME);
MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, MAILBOX_NAME);
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder()
.build("header: value\r\n\r\nbody"),
systemSession);
String taskId = when()
.delete(MAILBOX_NAME + "/messages")
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await");
assertThat(mailboxManager.getMailbox(mailboxPath, systemSession).getMailboxCounters(systemSession).getCount())
.isZero();
}
@Test
void userReprocessingShouldNotClearUnRelatedMailboxWhenDeleteMailboxContent() throws MailboxException {
with()
.put(MAILBOX_NAME);
String unRelatedMailbox = "unRelatedMailbox";
with()
.put(unRelatedMailbox);
MailboxPath mailboxPath = MailboxPath.forUser(USERNAME, unRelatedMailbox);
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
mailboxManager.getMailbox(mailboxPath, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder()
.build("header: value\r\n\r\nbody"),
systemSession);
String taskId = when()
.delete(MAILBOX_NAME + "/messages")
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await");
assertThat(mailboxManager.getMailbox(mailboxPath, systemSession).getMailboxCounters(systemSession).getCount())
.isEqualTo(1);
}
}
@Nested
class SideEffects {
@Test
void userReprocessingShouldPerformReprocessingWhenMail() throws Exception {
MailboxSession systemSession = mailboxManager.createSystemSession(USERNAME);
MailboxId mailboxId = mailboxManager.createMailbox(INBOX, systemSession).get();
ComposedMessageId createdMessage = mailboxManager.getMailbox(INBOX, systemSession)
.appendMessage(
MessageManager.AppendCommand.builder().build("header: value\r\n\r\nbody"),
systemSession).getId();
String taskId = given()
.queryParam("task", "reIndex")
.when()
.post()
.jsonPath()
.get("taskId");
given()
.basePath(TasksRoutes.BASE)
.when()
.get(taskId + "/await")
.then()
.body("status", Matchers.is("completed"));
ArgumentCaptor<MailboxMessage> messageCaptor = ArgumentCaptor.forClass(MailboxMessage.class);
ArgumentCaptor<MailboxId> mailboxIdCaptor = ArgumentCaptor.forClass(MailboxId.class);
ArgumentCaptor<Mailbox> mailboxCaptor2 = ArgumentCaptor.forClass(Mailbox.class);
verify(searchIndex).deleteAll(any(MailboxSession.class), mailboxIdCaptor.capture());
verify(searchIndex).add(any(MailboxSession.class), mailboxCaptor2.capture(), messageCaptor.capture());
verifyNoMoreInteractions(searchIndex);
assertThat(mailboxIdCaptor.getValue()).matches(capturedMailboxId -> capturedMailboxId.equals(mailboxId));
assertThat(mailboxCaptor2.getValue()).matches(mailbox -> mailbox.getMailboxId().equals(mailboxId));
assertThat(messageCaptor.getValue()).matches(message -> message.getMailboxId().equals(mailboxId)
&& message.getUid().equals(createdMessage.getUid()));
}
}
}
}