You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by rc...@apache.org on 2020/03/18 03:03:40 UTC

[james-project] 07/15: JAMES-3078 Download routes and tests

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

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

commit a84e855782044b1e741ff650bebdbc8551f378f6
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Wed Mar 11 13:41:55 2020 +0700

    JAMES-3078 Download routes and tests
---
 .../java/org/apache/james/util/ReactorUtils.java   |  16 ++
 .../org/apache/james/util/ReactorUtilsTest.java    |  91 ++++++++-
 .../integration/cucumber/DownloadStepdefs.java     |   2 +-
 .../james/jmap/draft/utils/DownloadPath.java       |  25 +--
 .../org/apache/james/jmap/http/DownloadRoutes.java | 217 +++++++++++++++++++++
 .../apache/james/jmap/http/DownloadRoutesTest.java |  58 ++++++
 6 files changed, 384 insertions(+), 25 deletions(-)

diff --git a/server/container/util/src/main/java/org/apache/james/util/ReactorUtils.java b/server/container/util/src/main/java/org/apache/james/util/ReactorUtils.java
index dd9ceba..a87ab17 100644
--- a/server/container/util/src/main/java/org/apache/james/util/ReactorUtils.java
+++ b/server/container/util/src/main/java/org/apache/james/util/ReactorUtils.java
@@ -37,6 +37,22 @@ public class ReactorUtils {
         return new StreamInputStream(byteArrays.toIterable(1).iterator());
     }
 
+    public static Flux<ByteBuffer> toChunks(InputStream inputStream, int bufferSize) {
+        return Flux.<ByteBuffer>generate(sink -> {
+                try {
+                    byte[] buffer = new byte[bufferSize];
+                    int read = inputStream.read(buffer);
+                    if (read >= 0) {
+                        sink.next(ByteBuffer.wrap(buffer, 0, read));
+                    } else {
+                        sink.complete();
+                    }
+                } catch (IOException e) {
+                    sink.error(e);
+                }
+            }).defaultIfEmpty(ByteBuffer.wrap(new byte[0]));
+    }
+
     private static class StreamInputStream extends InputStream {
         private static final int NO_MORE_DATA = -1;
 
diff --git a/server/container/util/src/test/java/org/apache/james/util/ReactorUtilsTest.java b/server/container/util/src/test/java/org/apache/james/util/ReactorUtilsTest.java
index c01d09a..06edbc5 100644
--- a/server/container/util/src/test/java/org/apache/james/util/ReactorUtilsTest.java
+++ b/server/container/util/src/test/java/org/apache/james/util/ReactorUtilsTest.java
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.commons.io.IOUtils;
@@ -32,12 +33,15 @@ import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Bytes;
+
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.scheduler.Schedulers;
 
 class ReactorUtilsTest {
+    static final int BUFFER_SIZE = 5;
 
     @Nested
     class ExecuteAndEmpty {
@@ -86,7 +90,7 @@ class ReactorUtilsTest {
     class ToInputStream {
 
         @Test
-        void givenAFluxOf3BytesShouldReadSuccessfullyTheWholeSource() throws IOException, InterruptedException {
+        void givenAFluxOf3BytesShouldReadSuccessfullyTheWholeSource() {
             byte[] bytes = "foo bar ...".getBytes(StandardCharsets.US_ASCII);
 
             Flux<ByteBuffer> source = Flux.fromIterable(Bytes.asList(bytes))
@@ -101,7 +105,7 @@ class ReactorUtilsTest {
         }
 
         @Test
-        void givenALongFluxBytesShouldReadSuccessfullyTheWholeSource() throws IOException, InterruptedException {
+        void givenALongFluxBytesShouldReadSuccessfullyTheWholeSource() {
             byte[] bytes = RandomStringUtils.randomAlphabetic(41111).getBytes(StandardCharsets.US_ASCII);
 
             Flux<ByteBuffer> source = Flux.fromIterable(Bytes.asList(bytes))
@@ -156,7 +160,7 @@ class ReactorUtilsTest {
         }
 
         @Test
-        void givenAFluxOf3BytesWithAnEmptyByteArrayShouldConsumeOnlyTheReadBytesAndThePrefetch() throws IOException, InterruptedException {
+        void givenAFluxOf3BytesWithAnEmptyByteArrayShouldConsumeOnlyTheReadBytesAndThePrefetch() throws IOException {
             AtomicInteger generateElements = new AtomicInteger(0);
             Flux<ByteBuffer> source = Flux.just(
                 new byte[] {0, 1, 2},
@@ -195,4 +199,85 @@ class ReactorUtilsTest {
             assertThat(generateElements.get()).isEqualTo(1);
         }
     }
+
+    @Nested
+    class ToChunks {
+        @Test
+        void givenInputStreamSmallerThanBufferSizeShouldReturnOneChunk() {
+            byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8);
+            InputStream source = new ByteArrayInputStream(bytes);
+
+            List<ByteBuffer> expected = ImmutableList.of(ByteBuffer.wrap(bytes));
+
+            List<ByteBuffer> chunks = ReactorUtils.toChunks(source, BUFFER_SIZE)
+                .collectList()
+                .block();
+
+            assertThat(chunks).isEqualTo(expected);
+        }
+
+        @Test
+        void givenInputStreamEqualToBufferSizeShouldReturnOneChunk() {
+            byte[] bytes = "foooo".getBytes(StandardCharsets.UTF_8);
+            InputStream source = new ByteArrayInputStream(bytes);
+
+            List<ByteBuffer> expected = ImmutableList.of(ByteBuffer.wrap(bytes));
+
+            List<ByteBuffer> chunks = ReactorUtils.toChunks(source, BUFFER_SIZE)
+                .collectList()
+                .block();
+
+            assertThat(chunks).isEqualTo(expected);
+        }
+
+        @Test
+        void givenInputStreamSlightlyBiggerThanBufferSizeShouldReturnTwoChunks() {
+            byte[] bytes = "foobar...".getBytes(StandardCharsets.UTF_8);
+            InputStream source = new ByteArrayInputStream(bytes);
+
+            List<ByteBuffer> expected = ImmutableList.of(
+                ByteBuffer.wrap("fooba".getBytes(StandardCharsets.UTF_8)),
+                ByteBuffer.wrap("r...".getBytes(StandardCharsets.UTF_8)));
+
+            List<ByteBuffer> chunks = ReactorUtils.toChunks(source, BUFFER_SIZE)
+                .collectList()
+                .block();
+
+            assertThat(chunks).isEqualTo(expected);
+        }
+
+        @Test
+        void givenInputStreamBiggerThanBufferSizeShouldReturnMultipleChunks() {
+            byte[] bytes = RandomStringUtils.randomAlphabetic(41111).getBytes(StandardCharsets.UTF_8);
+            InputStream source = new ByteArrayInputStream(bytes);
+
+            List<ByteBuffer> expected = Flux.fromIterable(Bytes.asList(bytes))
+                .window(BUFFER_SIZE)
+                .flatMapSequential(Flux::collectList)
+                .map(Bytes::toArray)
+                .map(ByteBuffer::wrap)
+                .collectList()
+                .block();
+
+            List<ByteBuffer> chunks = ReactorUtils.toChunks(source, BUFFER_SIZE)
+                .collectList()
+                .block();
+
+            assertThat(chunks).isEqualTo(expected);
+        }
+
+        @Test
+        void givenEmptyInputStreamShouldReturnEmptyChunk() {
+            byte[] bytes = "".getBytes(StandardCharsets.UTF_8);
+            InputStream source = new ByteArrayInputStream(bytes);
+
+            List<ByteBuffer> chunks = ReactorUtils.toChunks(source, BUFFER_SIZE)
+                .collectList()
+                .block();
+
+            List<ByteBuffer> expected = ImmutableList.of(ByteBuffer.wrap(bytes));
+
+            assertThat(chunks).isEqualTo(expected);
+        }
+    }
 }
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/cucumber/DownloadStepdefs.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/cucumber/DownloadStepdefs.java
index 9413cbb..961ada7 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/cucumber/DownloadStepdefs.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/cucumber/DownloadStepdefs.java
@@ -450,7 +450,7 @@ public class DownloadStepdefs {
     @Then("^the user should receive an attachment access token$")
     public void accessTokenResponse() throws Throwable {
         assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
-        assertThat(response.getHeaders("Content-Type")).extracting(Header::toString).containsExactly("Content-Type: text/plain");
+        assertThat(response.getHeaders("Content-Type")).extracting(Header::getValue).containsExactly("text/plain");
         assertThat(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)).isNotEmpty();
     }
 
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/DownloadPath.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/DownloadPath.java
index ac4ad2c..29a4e61 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/DownloadPath.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/utils/DownloadPath.java
@@ -19,32 +19,15 @@
 
 package org.apache.james.jmap.draft.utils;
 
-import java.util.List;
 import java.util.Optional;
 
-import com.google.common.base.Preconditions;
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-
 public class DownloadPath {
-
-    public static DownloadPath from(String path) {
-        Preconditions.checkArgument(!Strings.isNullOrEmpty(path), "'path' is mandatory");
-
-        // path =  /blobId/name
-        // idx  = 0 1      2
-        List<String> pathVariables = Splitter.on('/').splitToList(path);
-        Preconditions.checkArgument(pathVariables.size() >= 1 && pathVariables.size() <= 3, "'blobId' is mandatory");
-
-        String blobId = Iterables.get(pathVariables, 1, null);
-        Preconditions.checkArgument(!Strings.isNullOrEmpty(blobId), "'blobId' is mandatory");
-
-        return new DownloadPath(blobId, name(pathVariables));
+    public static DownloadPath ofBlobId(String blobId) {
+        return new DownloadPath(blobId, Optional.empty());
     }
 
-    private static Optional<String> name(List<String> pathVariables) {
-        return Optional.ofNullable(Strings.emptyToNull(Iterables.get(pathVariables, 2, null)));
+    public static DownloadPath of(String blobId, String name) {
+        return new DownloadPath(blobId, Optional.of(name));
     }
 
     private final String blobId;
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java
new file mode 100644
index 0000000..b9ff099
--- /dev/null
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/http/DownloadRoutes.java
@@ -0,0 +1,217 @@
+/****************************************************************
+ * 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.http;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static org.apache.james.jmap.HttpConstants.TEXT_PLAIN_CONTENT_TYPE;
+import static org.apache.james.jmap.http.JMAPUrls.DOWNLOAD;
+
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.jmap.JMAPRoutes;
+import org.apache.james.jmap.draft.api.SimpleTokenFactory;
+import org.apache.james.jmap.draft.exceptions.BadRequestException;
+import org.apache.james.jmap.draft.exceptions.InternalErrorException;
+import org.apache.james.jmap.draft.exceptions.UnauthorizedException;
+import org.apache.james.jmap.draft.model.AttachmentAccessToken;
+import org.apache.james.jmap.draft.utils.DownloadPath;
+import org.apache.james.mailbox.BlobManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.exception.BlobNotFoundException;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.Blob;
+import org.apache.james.mailbox.model.BlobId;
+import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.codec.EncoderUtil.Usage;
+import org.apache.james.util.ReactorUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+
+import io.netty.buffer.Unpooled;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+import reactor.netty.http.server.HttpServerRequest;
+import reactor.netty.http.server.HttpServerResponse;
+import reactor.netty.http.server.HttpServerRoutes;
+
+public class DownloadRoutes implements JMAPRoutes {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DownloadRoutes.class);
+    static final String BLOB_ID_PATH_PARAM = "blobId";
+    private static final String NAME_PATH_PARAM = "name";
+    private static final String DOWNLOAD_FROM_ID = String.format("%s/{%s}", DOWNLOAD, BLOB_ID_PATH_PARAM);
+    private static final String DOWNLOAD_FROM_ID_AND_NAME = String.format("%s/{%s}/{%s}", DOWNLOAD, BLOB_ID_PATH_PARAM, NAME_PATH_PARAM);
+    private static final int BUFFER_SIZE = 16 * 1024;
+
+    private final BlobManager blobManager;
+    private final SimpleTokenFactory simpleTokenFactory;
+    private final MetricFactory metricFactory;
+    private final AuthenticationReactiveFilter authenticationReactiveFilter;
+
+    @Inject
+    @VisibleForTesting
+    DownloadRoutes(BlobManager blobManager, SimpleTokenFactory simpleTokenFactory, MetricFactory metricFactory, AuthenticationReactiveFilter authenticationReactiveFilter) {
+        this.blobManager = blobManager;
+        this.simpleTokenFactory = simpleTokenFactory;
+        this.metricFactory = metricFactory;
+        this.authenticationReactiveFilter = authenticationReactiveFilter;
+    }
+
+    @Override
+    public Logger logger() {
+        return LOGGER;
+    }
+
+    @Override
+    public HttpServerRoutes define(HttpServerRoutes builder) {
+        return builder.post(DOWNLOAD_FROM_ID, this::postFromId)
+            .get(DOWNLOAD_FROM_ID, this::getFromId)
+            .post(DOWNLOAD_FROM_ID_AND_NAME, this::postFromIdAndName)
+            .get(DOWNLOAD_FROM_ID_AND_NAME, this::getFromIdAndName)
+            .options(DOWNLOAD_FROM_ID, CORS_CONTROL)
+            .options(DOWNLOAD_FROM_ID_AND_NAME, CORS_CONTROL);
+    }
+
+    private Mono<Void> postFromId(HttpServerRequest request, HttpServerResponse response) {
+        String blobId = request.param(BLOB_ID_PATH_PARAM);
+        DownloadPath downloadPath = DownloadPath.ofBlobId(blobId);
+        return post(request, response, downloadPath);
+    }
+
+    private Mono<Void> postFromIdAndName(HttpServerRequest request, HttpServerResponse response) {
+        String blobId = request.param(BLOB_ID_PATH_PARAM);
+        String name = request.param(NAME_PATH_PARAM);
+        DownloadPath downloadPath = DownloadPath.of(blobId, name);
+        return post(request, response, downloadPath);
+    }
+
+    private Mono<Void> post(HttpServerRequest request, HttpServerResponse response, DownloadPath downloadPath) {
+        return authenticationReactiveFilter.authenticate(request)
+            .flatMap(session -> Mono.from(metricFactory.runPublishingTimerMetric("JMAP-download-post",
+                respondAttachmentAccessToken(session, downloadPath, response)))
+            .onErrorResume(InternalErrorException.class, e -> handleInternalError(response, e))
+            .onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(response, e)))
+            .subscribeOn(Schedulers.elastic());
+    }
+
+    private Mono<Void> getFromId(HttpServerRequest request, HttpServerResponse response) {
+        String blobId = request.param(BLOB_ID_PATH_PARAM);
+        DownloadPath downloadPath = DownloadPath.ofBlobId(blobId);
+        return get(request, response, downloadPath);
+    }
+
+    private Mono<Void> getFromIdAndName(HttpServerRequest request, HttpServerResponse response) {
+        String blobId = request.param(BLOB_ID_PATH_PARAM);
+        try {
+            String name = URLDecoder.decode(request.param(NAME_PATH_PARAM), StandardCharsets.UTF_8.toString());
+            DownloadPath downloadPath = DownloadPath.of(blobId, name);
+            return get(request, response, downloadPath);
+        } catch (UnsupportedEncodingException e) {
+            throw new BadRequestException("Wrong url encoding", e);
+        }
+    }
+
+    private Mono<Void> get(HttpServerRequest request, HttpServerResponse response, DownloadPath downloadPath) {
+        return authenticationReactiveFilter.authenticate(request)
+            .flatMap(session -> Mono.from(metricFactory.runPublishingTimerMetric("JMAP-download-get",
+                download(session, downloadPath, response)))
+            .onErrorResume(InternalErrorException.class, e -> handleInternalError(response, e)))
+            .onErrorResume(UnauthorizedException.class, e -> handleAuthenticationFailure(response, e))
+            .subscribeOn(Schedulers.elastic());
+    }
+
+    private Mono<Void> respondAttachmentAccessToken(MailboxSession mailboxSession, DownloadPath downloadPath, HttpServerResponse resp) {
+        String blobId = downloadPath.getBlobId();
+        try {
+            if (!attachmentExists(mailboxSession, blobId)) {
+                return resp.status(NOT_FOUND).send();
+            }
+            AttachmentAccessToken attachmentAccessToken = simpleTokenFactory.generateAttachmentAccessToken(mailboxSession.getUser().asString(), blobId);
+            return resp.header(CONTENT_TYPE, TEXT_PLAIN_CONTENT_TYPE)
+                .status(OK)
+                .sendString(Mono.just(attachmentAccessToken.serialize()))
+                .then();
+        } catch (MailboxException e) {
+            throw new InternalErrorException("Error while asking attachment access token", e);
+        }
+    }
+
+    private boolean attachmentExists(MailboxSession mailboxSession, String blobId) throws MailboxException {
+        try {
+            blobManager.retrieve(BlobId.fromString(blobId), mailboxSession);
+            return true;
+        } catch (BlobNotFoundException e) {
+            return false;
+        }
+    }
+
+    @VisibleForTesting
+    Mono<Void> download(MailboxSession mailboxSession, DownloadPath downloadPath, HttpServerResponse response) {
+        String blobId = downloadPath.getBlobId();
+        try {
+            Blob blob = blobManager.retrieve(BlobId.fromString(blobId), mailboxSession);
+
+            return Mono.usingWhen(
+                Mono.fromCallable(blob::getStream),
+                stream -> downloadBlob(downloadPath.getName(), response, blob.getSize(), blob.getContentType(), stream),
+                stream -> Mono.fromRunnable(Throwing.runnable(stream::close).sneakyThrow())
+            );
+        } catch (BlobNotFoundException e) {
+            LOGGER.info("Attachment '{}' not found", blobId, e);
+            return response.status(NOT_FOUND).send();
+        } catch (MailboxException e) {
+            throw new InternalErrorException("Error while downloading", e);
+        }
+    }
+
+    private Mono<Void> downloadBlob(Optional<String> optionalName, HttpServerResponse response, long blobSize, String blobContentType, InputStream stream) {
+        return addContentDispositionHeader(optionalName, response)
+            .header("Content-Length", String.valueOf(blobSize))
+            .header(CONTENT_TYPE, blobContentType)
+            .status(OK)
+            .send(ReactorUtils.toChunks(stream, BUFFER_SIZE)
+                .map(Unpooled::wrappedBuffer))
+            .then();
+    }
+
+    private HttpServerResponse addContentDispositionHeader(Optional<String> optionalName, HttpServerResponse resp) {
+        return optionalName.map(name -> addContentDispositionHeaderRegardingEncoding(name, resp))
+            .orElse(resp);
+    }
+
+    private HttpServerResponse addContentDispositionHeaderRegardingEncoding(String name, HttpServerResponse resp) {
+        if (CharMatcher.ascii().matchesAllOf(name)) {
+            return resp.header("Content-Disposition", "attachment; filename=\"" + name + "\"");
+        } else {
+            return resp.header("Content-Disposition", "attachment; filename*=\"" + EncoderUtil.encodeEncodedWord(name, Usage.TEXT_TOKEN) + "\"");
+        }
+    }
+}
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DownloadRoutesTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DownloadRoutesTest.java
new file mode 100644
index 0000000..a1786c8
--- /dev/null
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/http/DownloadRoutesTest.java
@@ -0,0 +1,58 @@
+/****************************************************************
+ * 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.http;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.draft.api.SimpleTokenFactory;
+import org.apache.james.jmap.draft.exceptions.InternalErrorException;
+import org.apache.james.jmap.draft.utils.DownloadPath;
+import org.apache.james.mailbox.BlobManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MailboxSessionUtil;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.junit.Test;
+
+import reactor.netty.http.server.HttpServerResponse;
+
+public class DownloadRoutesTest {
+
+    @Test
+    public void downloadShouldFailWhenUnknownErrorOnAttachmentManager() throws Exception {
+        MailboxSession mailboxSession = MailboxSessionUtil.create(Username.of("User"));
+        BlobManager mockedBlobManager = mock(BlobManager.class);
+        when(mockedBlobManager.retrieve(any(), eq(mailboxSession)))
+            .thenThrow(new MailboxException());
+        AuthenticationReactiveFilter mockedAuthFilter = mock(AuthenticationReactiveFilter.class);
+        SimpleTokenFactory nullSimpleTokenFactory = null;
+
+        DownloadRoutes testee = new DownloadRoutes(mockedBlobManager, nullSimpleTokenFactory, new RecordingMetricFactory(), mockedAuthFilter);
+
+        HttpServerResponse resp = mock(HttpServerResponse.class);
+        assertThatThrownBy(() -> testee.download(mailboxSession, DownloadPath.ofBlobId("blobId"), resp).block())
+            .isInstanceOf(InternalErrorException.class);
+    }
+}


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