Java tutorial
/** * Copyright (c) 2015 Bosch Software Innovations GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.hawkbit.ddi.rest.resource; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertTrue; 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; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import org.apache.commons.lang3.RandomUtils; import org.eclipse.hawkbit.ddi.rest.resource.DdiArtifactDownloadTest.DownloadTestConfiguration; import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.ArtifactUpload; 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.junit.Before; import org.junit.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; import com.google.common.base.Charsets; import com.google.common.net.HttpHeaders; import io.qameta.allure.Description; import io.qameta.allure.Feature; import io.qameta.allure.Story; /** * Test artifact downloads from the controller. */ @Feature("Component Tests - Direct Device Integration API") @Story("Artifact Download Resource") @SpringBootTest(classes = { DownloadTestConfiguration.class }) public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { private static volatile int downLoadProgress = 0; private static volatile long shippedBytes = 0; private final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); @Before public void setup() { dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } @Test @Description("Tests non allowed requests on the artifact ressource, e.g. invalid URI, wrong if-match, wrong command.") public void invalidRequestsOnArtifactResource() throws Exception { // create target final Target target = testdataFactory.createTarget(); final List<Target> targets = Arrays.asList(target); // create ds final DistributionSet ds = testdataFactory.createDistributionSet(""); assignDistributionSet(ds, targets); // create artifact final int artifactSize = 5 * 1024; final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random), ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize)); // no artifact available mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455", target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound()); mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM", target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound()); // SM does not exist mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}", target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound()); mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM", target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound()); // test now consistent data to test allowed methods mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), 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", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isOk()); // test failed If-match mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), 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}", tenantAware.getCurrentTenant(), 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}", tenantAware.getCurrentTenant(), 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}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); mvc.perform(delete( "/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); mvc.perform(post( "/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isMethodNotAllowed()); } @Test @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) @Description("Tests valid downloads through the artifact resource by identifying the artifact not by ID but file name.") public void downloadArtifactThroughFileName() throws Exception { downLoadProgress = 1; shippedBytes = 0; assertThat(softwareModuleManagement.findAll(PAGE)).hasSize(0); // create target final Target target = testdataFactory.createTarget(); final List<Target> targets = Arrays.asList(target); // create ds final DistributionSet ds = testdataFactory.createDistributionSet(""); // create artifact final int artifactSize = 5 * 1024 * 1024; final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random), ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize)); // download fails as artifact is not yet assigned mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", 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}", tenantAware.getCurrentTenant(), 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())) .andReturn(); assertTrue("The same file that was uploaded is expected when downloaded", Arrays.equals(result.getResponse().getContentAsByteArray(), random)); // download complete assertThat(downLoadProgress).isEqualTo(10); assertThat(shippedBytes).isEqualTo(artifactSize); } @Test @Description("Tests valid MD5SUm file downloads through the artifact resource by identifying the artifact by ID.") public void downloadMd5sumThroughControllerApi() throws Exception { // create target final Target target = testdataFactory.createTarget(); // create ds final DistributionSet ds = testdataFactory.createDistributionSet(""); assignDistributionSet(ds, target); // create artifact final int artifactSize = 5 * 1024; final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random), getOsModule(ds), "file1", false, artifactSize)); // download final MvcResult result = mvc.perform(get( "/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM", tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())) .andExpect(status().isOk()).andExpect(header().string("Content-Disposition", "attachment;filename=" + artifact.getFilename() + ".MD5SUM")) .andReturn(); assertThat(result.getResponse().getContentAsByteArray()) .isEqualTo((artifact.getMd5Hash() + " " + artifact.getFilename()).getBytes(Charsets.US_ASCII)); } @Test @WithUser(principal = TestdataFactory.DEFAULT_CONTROLLER_ID, authorities = "ROLE_CONTROLLER", allSpPermissions = true) @Description("Test various HTTP range requests for artifact download, e.g. chunk download or download resume.") public void rangeDownloadArtifact() throws Exception { // create target final Target target = testdataFactory.createTarget(); final List<Target> targets = Arrays.asList(target); // create ds final DistributionSet ds = testdataFactory.createDistributionSet(""); final int resultLength = 5 * 1000 * 1024; // create artifact final byte random[] = RandomUtils.nextBytes(resultLength); final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random), getOsModule(ds), "file1", false, resultLength)); assertThat(random.length).isEqualTo(resultLength); // now assign and download successful assignDistributionSet(ds, targets); final int range = 100 * 1024; // 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}", tenantAware.getCurrentTenant(), 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(); 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}", tenantAware.getCurrentTenant(), 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)); // skip first 1000 Bytes and return the rest result = mvc.perform(get( "/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", tenantAware.getCurrentTenant(), 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)); // Start download from file end fails mvc.perform(get( "/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", tenantAware.getCurrentTenant(), 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")); // multipart download - first 20 bytes in 2 parts result = mvc.perform(get( "/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}", tenantAware.getCurrentTenant(), 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(); 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(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(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()); } @Configuration public static class DownloadTestConfiguration { @Bean public Listener cancelEventHandlerStubBean() { return new Listener(); } } private static class Listener { @EventListener(classes = DownloadProgressEvent.class) public static void listen(final DownloadProgressEvent event) { downLoadProgress++; shippedBytes += event.getShippedBytesSinceLast(); } } }