MockMvc integration testing - mocking the Azure ShareFileClient and StorageFileInputStream hangs indefinitely

59 Views Asked by At

I had problem with this test where this endpoint would hang (also tried with WebTestClient and it throws timeout, so the problem is not tied to mock mvc) on after return is called, changed some config and now I am getting HttpMessageNotWritableException.

I am almost sure it's something dealing with mocking the InputStreamResource type.

I have created minimal setup to reproduce this error.

Here is the test class:

import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.io.File;
import java.io.FileInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.msf.sits.health.mail.MailService;
import org.msf.sits.health.share.FileShareService;
import org.msf.sits.health.share.FileDownloadInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@AutoConfigureWebMvc
@ExtendWith(MockitoExtension.class)
public class FileDownloadControllerTest extends TestcontainersInitializer{

  @MockBean
  private MailService mailService;
  
  @MockBean
  private FileShareService fileShareService;

  @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private HttpMessageConverter[] httpMessageConverters;

  @Autowired
  private FilesDownloadController controller;

  @BeforeEach
  void setUp() {
    this.mockMvc = MockMvcBuilders.standaloneSetup(controller)
        .setMessageConverters(httpMessageConverters).build();
  }

  @Test
  @DisplayName("200 - GET /downloadz/{token}")
  void downloadzFileTest() throws Exception {
    var token = "GbdARtD594xVpQQqOHAg0cOQSvx5LKGD";

    File testFile = new File("src/test/resources", "integration/mocked-file.txt");
    InputStreamResource testInputStream = new InputStreamResource(new FileInputStream(
        testFile));
    when(fileShareService.getFileDownloadInfo(anyString(), anyString())).thenReturn(
        new FileDownloadInfo("mocked-file.txt", testInputStream.contentLength(),
            testInputStream)
    );

    MvcResult result = null;
    try {
      result = mockMvc.perform(MockMvcRequestBuilders
              .get("/downloadz/{token}", token)
              .contentType(MediaType.APPLICATION_OCTET_STREAM)
              .accept(MediaType.APPLICATION_OCTET_STREAM))
          .andDo(print())
          .andExpect(status().isOk()).andReturn();
    } catch (Exception e) {
      System.out.println("Exception: " + e.getMessage());
    }
  }
}

And here is the controller:

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.msf.sits.health.share.FileShareService;
import org.msf.sits.health.share.MedicalFileDownloadInfo;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequiredArgsConstructor
public class FilesDownloadController {

  private final FileShareService fileShareService;

  @GetMapping("/downloadz/{token}")
  public ResponseEntity<InputStreamResource> downloadFile(@PathVariable String token,
      HttpServletRequest request) {
    MedicalFileDownloadInfo medicalFileDownloadInfo = fileShareService.getMedicalFileDownloadInfo(
        token, request.getRemoteAddr());

    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_DISPOSITION,
        "attachment; filename=" + medicalFileDownloadInfo.getFileName());

    var response = ResponseEntity.ok()
        .headers(headers)
        .contentLength(medicalFileDownloadInfo.getContentLength())
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .body(medicalFileDownloadInfo.getContent());

    return response;
  }
}

POM.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.msf.sits</groupId>
    <artifactId>health</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>health</name>
    <description>health</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud-azure.version>5.5.0</spring-cloud-azure.version>
        <vaadin.version>24.1.7</vaadin.version>
        <tikka.version>2.9.1</tikka.version>
        <springdoc-openapi-starter-webmvc-ui.version>2.0.2</springdoc-openapi-starter-webmvc-ui.version>
        <spring-boot.testcontainers.version>3.2.0-M3</spring-boot.testcontainers.version>
        <testcontainers.version>1.19.3</testcontainers.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-starter-active-directory</artifactId>
        </dependency>
        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-starter-keyvault-secrets</artifactId>
        </dependency>
        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-starter-storage-file-share</artifactId>
        </dependency>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sendgrid</groupId>
            <artifactId>sendgrid-java</artifactId>
            <version>4.9.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc-openapi-starter-webmvc-ui.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tika</groupId>
            <artifactId>tika-core</artifactId>
            <version>${tikka.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- test dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>5.2.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-testcontainers</artifactId>
            <version>${spring-boot.testcontainers.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>${testcontainers.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <version>1.19.3</version>
            <scope>test</scope>
        </dependency>
      <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.8.0</version>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
        <scope>test</scope>
      </dependency>
      <!--  test dependencies -->
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.azure.spring</groupId>
                <artifactId>spring-cloud-azure-dependencies</artifactId>
                <version>${spring-cloud-azure.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-bom</artifactId>
                <version>${vaadin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-webapp-maven-plugin</artifactId>
                <version>2.12.0</version>
                <configuration>
                    <schemaVersion>v2</schemaVersion>
                    <subscriptionId>${env.SUBSCRIPTION_ID}</subscriptionId>
                    <resourceGroup>${env.RESOURCE_GROUP}</resourceGroup>
                    <appName>${env.APP_NAME}</appName>
                    <pricingTier>${env.TIER}</pricingTier>
                    <region>${env.REGION}</region>
                    <runtime>
                        <os>Linux</os>
                        <javaVersion>Java 17</javaVersion>
                        <webContainer>Tomcat 10.0</webContainer>
                    </runtime>
                    <deployment>
                        <resources>
                            <resource>
                                <directory>${project.basedir}/target</directory>
                                <includes>
                                    <include>*.war</include>
                                </includes>
                            </resource>
                        </resources>
                    </deployment>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jxr-plugin</artifactId>
                <version>3.3.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-checkstyle-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <configLocation>sun_checks.xml</configLocation>
                </configuration>
                <reportSets>
                    <reportSet>
                        <reports>
                            <report>checkstyle</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-pmd-plugin</artifactId>
                <version>3.21.0</version>
            </plugin>
        </plugins>
    </reporting>

    <profiles>
        <profile>
            <id>production</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>com.vaadin</groupId>
                        <artifactId>vaadin-maven-plugin</artifactId>
                        <version>${vaadin.version}</version>
                        <executions>
                            <execution>
                                <id>frontend</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>prepare-frontend</goal>
                                    <goal>build-frontend</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


</project>
1

There are 1 best solutions below

0
solujic On

In order to fix the problem of infinite hanging caused by the stream mock I had to mock the storageFileInputStream and it's read method in a following way:

The piece of code that sets up the mocks:

    ShareFileClient shareFileClient = Mockito.mock(ShareFileClient.class,
        Mockito.RETURNS_DEEP_STUBS);
    StorageFileInputStream storageFileInputStream = Mockito.mock(StorageFileInputStream.class);

    when(shareClient.getFileClient(anyString())).thenReturn(shareFileClient);
    when(shareFileClient.openInputStream()).thenReturn(storageFileInputStream);

    byte[] mockData = "Mocked File data!".getBytes();
    mockStorageFileInputStreamRead(storageFileInputStream, mockData);

Method with implementation of read() mock:

  private static void mockStorageFileInputStreamRead(StorageFileInputStream storageFileInputStream,
      byte[] mockData)
      throws IOException {
    AtomicInteger bytesRead = new AtomicInteger(0);

    when(storageFileInputStream.read(any(byte[].class), anyInt(), anyInt())).thenAnswer(
        invocation -> {
          Object[] args = invocation.getArguments();
          byte[] output = (byte[]) args[0];
          int offset = (int) args[1];
          int len = (int) args[2];

          if (bytesRead.get() >= mockData.length) {
            return -1;
          }

          int bytesToRead = Math.min(mockData.length - bytesRead.get(), len);
          System.arraycopy(mockData, bytesRead.get(), output, offset, bytesToRead);
          bytesRead.addAndGet(bytesToRead);

          return bytesToRead;
        });
  }