blob: 67cd602b8c102134c32e00f95d5a82e1f1337fc5 [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.jmap.rfc8621.distributed;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.requestSpecification;
import static io.restassured.RestAssured.with;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.james.jmap.rfc8621.contract.Fixture.authScheme;
import static org.apache.james.jmap.rfc8621.contract.Fixture.baseRequestSpecBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.charset.StandardCharsets;
import org.apache.james.CassandraExtension;
import org.apache.james.CassandraRabbitMQJamesConfiguration;
import org.apache.james.CassandraRabbitMQJamesServerMain;
import org.apache.james.DockerOpenSearchExtension;
import org.apache.james.GuiceJamesServer;
import org.apache.james.JamesServerBuilder;
import org.apache.james.JamesServerExtension;
import org.apache.james.backends.cassandra.StatementRecorder;
import org.apache.james.backends.cassandra.TestingSession;
import org.apache.james.backends.cassandra.init.SessionWithInitializedTablesFactory;
import org.apache.james.jmap.JmapGuiceProbe;
import org.apache.james.jmap.http.UserCredential;
import org.apache.james.jmap.rfc8621.contract.Fixture;
import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags;
import org.apache.james.mailbox.MessageManager.AppendCommand;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.modules.AwsS3BlobStoreExtension;
import org.apache.james.modules.MailboxProbeImpl;
import org.apache.james.modules.RabbitMQExtension;
import org.apache.james.modules.TestJMAPServerModule;
import org.apache.james.modules.blobstore.BlobStoreConfiguration;
import org.apache.james.utils.DataProbeImpl;
import org.apache.james.utils.GuiceProbe;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import com.datastax.oss.driver.api.core.CqlSession;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.multibindings.Multibinder;
import io.netty.handler.codec.http.HttpHeaderNames;
@Tag(CategoryTags.BASIC_FEATURE)
class ReadLevelTest {
private static class TestingSessionProbe implements GuiceProbe {
private final TestingSession testingSession;
@Inject
private TestingSessionProbe(TestingSession testingSession) {
this.testingSession = testingSession;
}
public TestingSession getTestingSession() {
return testingSession;
}
}
private static class TestingSessionModule extends AbstractModule {
@Override
protected void configure() {
Multibinder.newSetBinder(binder(), GuiceProbe.class)
.addBinding()
.to(TestingSessionProbe.class);
bind(CqlSession.class).to(TestingSession.class);
}
@Provides
@Singleton
TestingSession provideSession(SessionWithInitializedTablesFactory factory) {
return new TestingSession(factory.get());
}
}
@RegisterExtension
static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
CassandraRabbitMQJamesConfiguration.builder()
.workingDirectory(tmpDir)
.configurationFromClasspath()
.enableJMAP()
.blobStore(BlobStoreConfiguration.builder()
.cassandra()
.disableCache()
.deduplication()
.noCryptoConfig())
.build())
.extension(new DockerOpenSearchExtension())
.extension(new CassandraExtension())
.extension(new RabbitMQExtension())
.extension(new AwsS3BlobStoreExtension())
.server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
.overrideWith(new TestJMAPServerModule())
.overrideWith(new TestingSessionModule()))
.build();
private MessageId messageId;
@BeforeEach
void setUp(GuiceJamesServer server) throws Exception {
server.getProbe(DataProbeImpl.class)
.fluent()
.addDomain(Fixture.DOMAIN().asString())
.addUser(Fixture.BOB().asString(), Fixture.BOB_PASSWORD());
requestSpecification = baseRequestSpecBuilder(server)
.setAuth(authScheme(new UserCredential(Fixture.BOB(), Fixture.BOB_PASSWORD())))
.build();
server.getProbe(MailboxProbeImpl.class).createMailbox(MailboxPath.inbox(Fixture.BOB()));
messageId = server.getProbe(MailboxProbeImpl.class)
.appendMessage(Fixture.BOB().asString(), MailboxPath.inbox(Fixture.BOB()), AppendCommand.from(createMessage()))
.getMessageId();
Thread.sleep(1000); // Await for the preview to be computed
}
@Test
void gettingEmailMetadataShouldNotReadBlobs(GuiceJamesServer server) {
StatementRecorder statementRecorder = server.getProbe(TestingSessionProbe.class)
.getTestingSession()
.recordStatements();
String request = "{" +
" \"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"]," +
" \"methodCalls\": [[" +
" \"Email/get\"," +
" {" +
" \"accountId\": \"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\", \"mailboxIds\", \"blobId\", " +
" \"threadId\", \"receivedAt\"]" +
" }," +
" \"c1\"]]" +
"} ";
with()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post();
assertThat(statementRecorder.listExecutedStatements(
StatementRecorder.Selector.preparedStatementStartingWith("SELECT * FROM blobs")))
.hasSize(0);
}
@Test
void gettingEmailHeadersShouldReadBlobOnce(GuiceJamesServer server) {
StatementRecorder statementRecorder = server.getProbe(TestingSessionProbe.class)
.getTestingSession()
.recordStatements();
String request = "{" +
" \"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"]," +
" \"methodCalls\": [[" +
" \"Email/get\"," +
" {" +
" \"accountId\": \"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\", \"mailboxIds\", \"blobId\", " +
" \"threadId\", \"receivedAt\", \"messageId\", \"inReplyTo\", " +
" \"references\", \"to\", \"cc\", \"bcc\", \"from\", \"sender\", " +
" \"replyTo\", \"subject\", \"headers\", \"header:anything\"]" +
" }," +
" \"c1\"]]" +
"} ";
with()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post();
assertThat(statementRecorder.listExecutedStatements(
StatementRecorder.Selector.preparedStatementStartingWith("SELECT * FROM blobs")))
.hasSize(1);
}
@Test
void gettingEmailFastViewShouldReadBlobOnce(GuiceJamesServer server) {
StatementRecorder statementRecorder = server.getProbe(TestingSessionProbe.class)
.getTestingSession()
.recordStatements();
String request = "{" +
" \"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"]," +
" \"methodCalls\": [[" +
" \"Email/get\"," +
" {" +
" \"accountId\": \"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\", \"mailboxIds\", \"blobId\", " +
" \"threadId\", \"receivedAt\", \"messageId\", \"inReplyTo\", " +
" \"references\", \"to\", \"cc\", \"bcc\", \"from\", \"sender\", " +
" \"replyTo\", \"subject\", \"headers\", \"header:anything\", " +
" \"preview\", \"hasAttachment\"]" +
" }," +
" \"c1\"]]" +
"} ";
with()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post();
assertThat(statementRecorder.listExecutedStatements(
StatementRecorder.Selector.preparedStatementStartingWith("SELECT * FROM blobs")))
.hasSize(1);
}
@Test
void gettingEmailFastViewShouldReadBlobTwiceUponCacheMisses(GuiceJamesServer server) {
server.getProbe(JmapGuiceProbe.class).clearMessageFastViewProjection();
StatementRecorder statementRecorder = server.getProbe(TestingSessionProbe.class)
.getTestingSession()
.recordStatements();
String request = "{" +
" \"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"]," +
" \"methodCalls\": [[" +
" \"Email/get\"," +
" {" +
" \"accountId\": \"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\", \"mailboxIds\", \"blobId\", " +
" \"threadId\", \"receivedAt\", \"messageId\", \"inReplyTo\", " +
" \"references\", \"to\", \"cc\", \"bcc\", \"from\", \"sender\", " +
" \"replyTo\", \"subject\", \"headers\", \"header:anything\", " +
" \"preview\", \"hasAttachment\"]" +
" }," +
" \"c1\"]]" +
"} ";
with()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post();
assertThat(statementRecorder.listExecutedStatements(
StatementRecorder.Selector.preparedStatementStartingWith("SELECT * FROM blobs")))
.hasSize(2);
}
@Test
void previewMissesShouldPopulateTheProjection(GuiceJamesServer server) {
server.getProbe(JmapGuiceProbe.class).clearMessageFastViewProjection();
String request = "{" +
" \"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"]," +
" \"methodCalls\": [[" +
" \"Email/get\"," +
" {" +
" \"accountId\": \"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\", \"mailboxIds\", \"blobId\", " +
" \"threadId\", \"receivedAt\", \"messageId\", \"inReplyTo\", " +
" \"references\", \"to\", \"cc\", \"bcc\", \"from\", \"sender\", " +
" \"replyTo\", \"subject\", \"headers\", \"header:anything\", " +
" \"preview\", \"hasAttachment\"]" +
" }," +
" \"c1\"]]" +
"} ";
given()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post()
.then()
.statusCode(SC_OK);
StatementRecorder statementRecorder = server.getProbe(TestingSessionProbe.class)
.getTestingSession()
.recordStatements();
with()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post();
assertThat(statementRecorder.listExecutedStatements(
StatementRecorder.Selector.preparedStatementStartingWith("SELECT * FROM blobs")))
.hasSize(1);
}
@Test
void gettingEmailBodyShouldReadBlobTwice(GuiceJamesServer server) {
StatementRecorder statementRecorder = server.getProbe(TestingSessionProbe.class)
.getTestingSession()
.recordStatements();
String request = "{" +
" \"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:mail\"]," +
" \"methodCalls\": [[" +
" \"Email/get\"," +
" {" +
" \"accountId\": \"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\", \"mailboxIds\", \"blobId\", " +
" \"threadId\", \"receivedAt\", \"messageId\", \"inReplyTo\", " +
" \"references\", \"to\", \"cc\", \"bcc\", \"from\", \"sender\", " +
" \"replyTo\", \"subject\", \"headers\", \"header:anything\", " +
" \"preview\", \"hasAttachment\", \"bodyStructure\", \"textBody\", \"htmlBody\",\n" +
" \"attachments\", \"bodyValues\"]" +
" }," +
" \"c1\"]]" +
"} ";
with()
.header(HttpHeaderNames.ACCEPT.toString(), Fixture.ACCEPT_RFC8621_VERSION_HEADER())
.body(request)
.post();
assertThat(statementRecorder.listExecutedStatements(
StatementRecorder.Selector.preparedStatementStartingWith("SELECT * FROM blobs")))
.hasSize(2);
}
private Message createMessage() throws Exception {
return Message.Builder
.of()
.setSubject("test")
.setSender(Fixture.ANDRE().asString())
.setFrom(Fixture.ANDRE().asString())
.setSubject("World domination \r\n" +
" and this is also part of the header")
.setBody("testmail", StandardCharsets.UTF_8)
.build();
}
}