[#2918] Refactor FileStreamingUtil (#2921)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2026-02-13 17:11:43 +02:00
committed by GitHub
parent ef3900a31c
commit 520b887b70
6 changed files with 360 additions and 400 deletions

View File

@@ -182,12 +182,11 @@ public class DdiRootController implements DdiRootControllerRestApi {
? logDownload(RequestResponseContextHolder.getHttpServletRequest(), target, module.getId())
: null; // range request - could have too many - so doesn't check action, don't log action status, and don't publish events
result = FileStreamingUtil.writeFileResponse(file, artifact.getFilename(), artifact.getCreatedAt(),
RequestResponseContextHolder.getHttpServletResponse(),
RequestResponseContextHolder.getHttpServletRequest(),
RequestResponseContextHolder.getHttpServletRequest(), RequestResponseContextHolder.getHttpServletResponse(),
(length, shippedSinceLastEvent, total) -> {
if (actionStatus != null) {
eventPublisher.publishEvent(new DownloadProgressEvent(
AccessContext.tenant(), actionStatus.getId(), shippedSinceLastEvent));
eventPublisher.publishEvent(
new DownloadProgressEvent(AccessContext.tenant(), actionStatus.getId(), shippedSinceLastEvent));
}
});
}

View File

@@ -10,7 +10,19 @@
package org.eclipse.hawkbit.ddi.rest.resource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.context.AccessContext.tenant;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.springframework.http.HttpHeaders.ACCEPT_RANGES;
import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION;
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
import static org.springframework.http.HttpHeaders.CONTENT_RANGE;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.HttpHeaders.ETAG;
import static org.springframework.http.HttpHeaders.LAST_MODIFIED;
import static org.springframework.http.HttpHeaders.RANGE;
import static org.springframework.http.HttpStatus.PARTIAL_CONTENT;
import static org.springframework.http.HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -30,7 +42,6 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.eclipse.hawkbit.context.AccessContext;
import org.eclipse.hawkbit.ddi.rest.resource.DdiArtifactDownloadTest.DownloadTestConfiguration;
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
import org.eclipse.hawkbit.repository.model.Artifact;
@@ -39,6 +50,8 @@ import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@@ -46,7 +59,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MvcResult;
/**
@@ -58,6 +71,8 @@ import org.springframework.test.web.servlet.MvcResult;
@SpringBootTest(classes = { DownloadTestConfiguration.class })
class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
private static final String DOWNLOAD_FN = "/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}";
private static int downloadProgress = 0;
private static long shippedBytes = 0;
@@ -76,86 +91,65 @@ class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
// create target
final Target target = testdataFactory.createTarget();
final List<Target> targets = Collections.singletonList(target);
// create ds
final DistributionSet ds = testdataFactory.createDistributionSet("");
// create artifact
final int artifactSize = 5 * 1024;
final byte[] random = nextBytes(artifactSize);
final Artifact artifact = artifactManagement.create(new ArtifactUpload(
new ByteArrayInputStream(random), null, artifactSize, null,
findFirstModuleByType(ds, osType).orElseThrow().getId(), "file1", false));
new ByteArrayInputStream(random),
null, artifactSize, null, findFirstModuleByType(ds, osType).orElseThrow().getId(), "file1", false));
assignDistributionSet(ds, targets);
// no artifact available
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455",
target.getControllerId(), getOsModule(ds)))
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), "123455"))
.andExpect(status().isNotFound());
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM",
target.getControllerId(), getOsModule(ds)))
mvc.perform(get(DOWNLOAD_FN + ".MD5SUM", tenant(), target.getControllerId(), getOsModule(ds), "123455"))
.andExpect(status().isNotFound());
// SM does not exist
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}",
target.getControllerId(), artifact.getFilename()))
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), "1234567890", artifact.getFilename()))
.andExpect(status().isNotFound());
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM",
target.getControllerId(), artifact.getFilename()))
mvc.perform(get(DOWNLOAD_FN + ".MD5SUM", tenant(), target.getControllerId(), "1234567890", artifact.getFilename()))
.andExpect(status().isNotFound());
// test now consistent data to test allowed methods
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header(HttpHeaders.IF_MATCH, artifact.getSha1Hash()))
.andExpect(status().isOk());
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(get(DOWNLOAD_FN + ".MD5SUM",
tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isOk());
// test failed If-match
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header(HttpHeaders.IF_MATCH, "fsjkhgjfdhg"))
.andExpect(status().isPreconditionFailed());
// test invalid range
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header("Range", "bytes=1-10,hdsfjksdh"))
.andExpect(header().string("Content-Range", "bytes */" + 5 * 1024))
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header(RANGE, "bytes=1-10,hdsfjksdh"))
.andExpect(header().string(CONTENT_RANGE, "bytes */" + 5 * 1024))
.andExpect(status().isRequestedRangeNotSatisfiable());
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header("Range", "bytes=100-10"))
.andExpect(header().string("Content-Range", "bytes */" + 5 * 1024))
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header(RANGE, "bytes=100-10"))
.andExpect(header().string(CONTENT_RANGE, "bytes */" + 5 * 1024))
.andExpect(status().isRequestedRangeNotSatisfiable());
// not allowed methods
mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(put(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isMethodNotAllowed());
mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(delete(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isMethodNotAllowed());
mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(post(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isMethodNotAllowed());
mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(put(DOWNLOAD_FN + ".MD5SUM", tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isMethodNotAllowed());
mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(delete(DOWNLOAD_FN + ".MD5SUM", tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isMethodNotAllowed());
mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(post(DOWNLOAD_FN + ".MD5SUM", tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isMethodNotAllowed());
}
@@ -174,32 +168,26 @@ class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
// create target
final Target target = testdataFactory.createTarget();
final List<Target> targets = Collections.singletonList(target);
// create ds
final DistributionSet ds = testdataFactory.createDistributionSet("");
// create artifact
final int artifactSize = (int) quotaManagement.getMaxArtifactSize();
final byte[] random = nextBytes(artifactSize);
final Artifact artifact = artifactManagement.create(new ArtifactUpload(
new ByteArrayInputStream(random), null, artifactSize, null,
findFirstModuleByType(ds, osType).orElseThrow().getId(), "file1", false));
new ByteArrayInputStream(random),
null, artifactSize, null, findFirstModuleByType(ds, osType).orElseThrow().getId(), "file1", false));
// download fails as artifact is not yet assigned
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
target.getControllerId(), getOsModule(ds), artifact.getFilename()))
mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isNotFound());
// now assign and download successful
assignDistributionSet(ds, targets);
final MvcResult result = mvc.perform(get(
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
final MvcResult result = mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string("Content-Disposition", "attachment;filename=" + artifact.getFilename()))
.andExpect(content().contentType(APPLICATION_OCTET_STREAM))
.andExpect(header().string(ACCEPT_RANGES, "bytes"))
.andExpect(header().string(LAST_MODIFIED, dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string(CONTENT_DISPOSITION, "attachment;filename=" + artifact.getFilename()))
.andReturn();
assertArrayEquals(result.getResponse().getContentAsByteArray(), random, "The same file that was uploaded is expected when downloaded");
@@ -215,29 +203,24 @@ class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
* Tests valid MD5SUm file downloads through the artifact resource by identifying the artifact by ID.
*/
@Test
void downloadMd5sumThroughControllerApi() throws Exception {
void downloadMd5SumThroughControllerApi() throws Exception {
// create target
final Target target = testdataFactory.createTarget();
// create ds
final DistributionSet ds = testdataFactory.createDistributionSet("");
// create artifact
final int artifactSize = 5 * 1024;
final byte[] random = nextBytes(artifactSize);
final Artifact artifact = artifactManagement.create(new ArtifactUpload(
new ByteArrayInputStream(random), null, artifactSize, null,
getOsModule(ds), "file1", false));
new ByteArrayInputStream(random), null, artifactSize, null, getOsModule(ds), "file1", false));
assignDistributionSet(ds, target);
// download
final MvcResult result = mvc.perform(get(
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
final MvcResult result = mvc.perform(
get(DOWNLOAD_FN + ".MD5SUM", tenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
.andExpect(status().isOk())
.andExpect(header().string("Content-Disposition",
"attachment;filename=" + artifact.getFilename() + ".MD5SUM"))
.andExpect(header().string(CONTENT_DISPOSITION, "attachment;filename=" + artifact.getFilename() + ".MD5SUM"))
.andReturn();
assertThat(result.getResponse().getContentAsByteArray())
@@ -253,124 +236,80 @@ class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
// create target
final Target target = testdataFactory.createTarget();
final List<Target> targets = Collections.singletonList(target);
// create ds
final DistributionSet ds = testdataFactory.createDistributionSet("");
final int resultLength = (int) quotaManagement.getMaxArtifactSize();
// create artifact
final int resultLength = (int) quotaManagement.getMaxArtifactSize();
final byte[] random = nextBytes(resultLength);
final Artifact artifact = artifactManagement.create(new ArtifactUpload(
new ByteArrayInputStream(random), null, resultLength, null,
getOsModule(ds), "file1", false));
new ByteArrayInputStream(random), null, resultLength, null, getOsModule(ds), "file1", false));
assertThat(random).hasSize(resultLength);
// now assign and download successful
assignDistributionSet(ds, targets);
final int range = resultLength / 50;
// full file download with standard range request
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (int i = 0; i < resultLength / range; i++) {
final String rangeString = i * range + "-" + ((i + 1) * range - 1);
final MvcResult result = mvc.perform(get(
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), "file1").header("Range",
"bytes=" + rangeString))
.andExpect(status().isPartialContent())
.andExpect(header().string("ETag", artifact.getSha1Hash()))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().longValue("Content-Length", range))
.andExpect(header().string("Content-Range", "bytes " + rangeString + "/" + resultLength))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"))
.andReturn();
final String rangeString = (i * range) + "-" + ((i + 1) * range - 1);
final MvcResult result = getRange(target, ds, artifact, rangeString, PARTIAL_CONTENT, range, rangeString + "/" + resultLength);
outputStream.write(result.getResponse().getContentAsByteArray());
}
assertThat(outputStream.toByteArray()).isEqualTo(random);
// return last 1000 Bytes
MvcResult result = mvc.perform(
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), "file1")
.header("Range", "bytes=-1000"))
.andExpect(status().isPartialContent())
.andExpect(header().string("ETag", artifact.getSha1Hash()))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().longValue("Content-Length", 1000))
.andExpect(header().string("Content-Range",
"bytes " + (resultLength - 1000) + "-" + (resultLength - 1) + "/" + resultLength))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"))
.andReturn();
assertThat(result.getResponse().getContentAsByteArray())
.isEqualTo(Arrays.copyOfRange(random, resultLength - 1000, resultLength));
MvcResult result = getRange(
target, ds, artifact, "-1000",
PARTIAL_CONTENT, 1000, (resultLength - 1000) + "-" + (resultLength - 1) + "/" + resultLength);
assertThat(result.getResponse().getContentAsByteArray()).isEqualTo(Arrays.copyOfRange(random, resultLength - 1000, resultLength));
// skip first 1000 Bytes and return the rest
result = mvc.perform(
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), "file1")
.header("Range", "bytes=1000-"))
.andExpect(status().isPartialContent())
.andExpect(header().string("ETag", artifact.getSha1Hash()))
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().longValue("Content-Length", resultLength - 1000))
.andExpect(header().string("Content-Range", "bytes " + 1000 + "-" + (resultLength - 1) + "/" + resultLength))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"))
.andReturn();
assertThat(result.getResponse().getContentAsByteArray())
.isEqualTo(Arrays.copyOfRange(random, 1000, resultLength));
result = getRange(
target, ds, artifact, "1000-",
PARTIAL_CONTENT, resultLength - 1000, 1000 + "-" + (resultLength - 1) + "/" + resultLength);
assertThat(result.getResponse().getContentAsByteArray()).isEqualTo(Arrays.copyOfRange(random, 1000, resultLength));
// Start download from file end fails
mvc.perform(
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), "file1")
.header("Range", "bytes=" + random.length + "-"))
.andExpect(status().isRequestedRangeNotSatisfiable())
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string("Content-Range", "bytes */" + random.length))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"));
getRange(target, ds, artifact, random.length + "-", REQUESTED_RANGE_NOT_SATISFIABLE, null, "*/" + resultLength);
// multipart download - first 20 bytes in 2 parts
result = mvc.perform(
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
AccessContext.tenant(), target.getControllerId(), getOsModule(ds), "file1")
.header("Range", "bytes=0-9,10-19"))
.andExpect(status().isPartialContent())
.andExpect(header().string("ETag", artifact.getSha1Hash()))
.andExpect(content().contentType("multipart/byteranges; boundary=THIS_STRING_SEPARATES_MULTIPART"))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"))
.andReturn();
result = getRange(target, ds, artifact, "0-9,10-19", PARTIAL_CONTENT, null, null);
outputStream.reset();
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(("Content-Range: bytes 0-9/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1));
outputStream.write("--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(("Content-Type: application/octet-stream\r\n").getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(("Content-Range: bytes 0-9/" + resultLength + "\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(Arrays.copyOfRange(random, 0, 10));
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
outputStream
.write(("Content-Range: bytes 10-19/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(("Content-Type: application/octet-stream\r\n").getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(("Content-Range: bytes 10-19/" + resultLength + "\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
outputStream.write(Arrays.copyOfRange(random, 10, 20));
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART--".getBytes(StandardCharsets.ISO_8859_1));
assertThat(result.getResponse().getContentAsByteArray()).isEqualTo(outputStream.toByteArray());
}
private MvcResult getRange(
final Target target, final DistributionSet ds, final Artifact artifact, final String range,
final HttpStatus expectStatus, final Integer expectedContentLength, final String expectContentRange) throws Exception {
return mvc.perform(get(DOWNLOAD_FN, tenant(), target.getControllerId(), getOsModule(ds), "file1").header(RANGE, "bytes=" + range))
.andExpect(status().is(expectStatus.value()))
.andExpect(header().string(
CONTENT_TYPE,
range.contains(",")
? "multipart/byteranges; boundary=THIS_STRING_SEPARATES_MULTIPART"
: APPLICATION_OCTET_STREAM.toString()))
.andExpect(header().string(
ETAG, new HeaderMatcher(expectStatus.value() == 419 ? null : ('"' + artifact.getSha1Hash() + '"'))))
.andExpect(header().string(ACCEPT_RANGES, "bytes"))
.andExpect(header().string(LAST_MODIFIED, dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string(
CONTENT_LENGTH, new HeaderMatcher(expectedContentLength == null ? null : String.valueOf(expectedContentLength))))
.andExpect(header().string(
CONTENT_RANGE, new HeaderMatcher(expectContentRange == null ? null : ("bytes " + expectContentRange))))
.andExpect(header().string(CONTENT_DISPOSITION, "attachment;filename=file1"))
.andReturn();
}
@Configuration
@@ -380,7 +319,25 @@ class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
Listener cancelEventHandlerStubBean() {
return new Listener();
}
}
private static class HeaderMatcher extends BaseMatcher<String> {
private final String expectedValue;
private HeaderMatcher(final String expectedValue) {
this.expectedValue = expectedValue;
}
@Override
public boolean matches(final Object actual) {
return expectedValue == null ? actual == null : expectedValue.equals(String.valueOf(actual));
}
@Override
public void describeTo(final Description description) {
description.appendValue(expectedValue);
}
}
private static class Listener {