Skip to content
1 change: 1 addition & 0 deletions gp2gp-translator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
testImplementation 'org.skyscreamer:jsonassert:1.5.3'
testImplementation 'org.awaitility:awaitility:4.2.2'
testImplementation 'io.findify:s3mock_2.13:0.2.6'
testImplementation 'org.testcontainers:testcontainers-azure:2.0.5'

pitest 'com.arcmutate:base:1.3.2'
pitest 'com.arcmutate:pitest-git-plugin:2.0.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package uk.nhs.adaptors.pss.translator.storage;

import com.azure.core.util.BinaryData;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.azure.AzuriteContainer;

import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertNotNull;

public class AzureStorageServiceTest {

private static final String CONTAINER_NAME = "azurecontainer";
private static final String FILE_NAME = "testfile.txt";

private AzureStorageService azureStorageService;
private AzuriteContainer azuriteContainer;
private BlobServiceClient blobServiceClient;

@BeforeEach
void setUp() {
azuriteContainer = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0");
azuriteContainer.start();

blobServiceClient = new BlobServiceClientBuilder()
.connectionString(azuriteContainer.getConnectionString())
.buildClient();
blobServiceClient.createBlobContainer(CONTAINER_NAME);

StorageServiceConfiguration config = new StorageServiceConfiguration();
config.setContainerName(CONTAINER_NAME);

azureStorageService = new AzureStorageService(blobServiceClient, config);
}

@AfterEach
void tearDown() {
azuriteContainer.stop();
}

@Test
void uploadToStorageTest() {
String uploadContent = "uploadcontent";

azureStorageService.uploadFile(FILE_NAME, uploadContent.getBytes(StandardCharsets.UTF_8));

byte[] downloaded = blobServiceClient
.getBlobContainerClient(CONTAINER_NAME)
.getBlobClient(FILE_NAME).downloadContent().toBytes();

Assertions.assertEquals(uploadContent, new String(downloaded, StandardCharsets.UTF_8));
}

@Test
void downloadFromStorageTest() {

String fileContent = "dummy-content";

blobServiceClient
.getBlobContainerClient(CONTAINER_NAME)
.getBlobClient(FILE_NAME).upload(BinaryData.fromString(fileContent));

byte[] response = azureStorageService.downloadFile(FILE_NAME);
String downloadedContent = new String(response, StandardCharsets.UTF_8);

assertNotNull(response);
Assertions.assertEquals(fileContent, downloadedContent);
}

@Test
void deleteFileTest() {

String fileContent = "dummy-content";
blobServiceClient
.getBlobContainerClient(CONTAINER_NAME)
.getBlobClient(FILE_NAME).upload(BinaryData.fromString(fileContent));

azureStorageService.deleteFile(FILE_NAME);

Exception exception = Assertions.assertThrows(Exception.class, () -> azureStorageService.downloadFile(FILE_NAME));

Assertions.assertEquals("Status code 404, BlobNotFound", exception.getMessage());
}

@Test
void getFileLocationTest() {
String fileContent = "dummy-content";
blobServiceClient
.getBlobContainerClient(CONTAINER_NAME)
.getBlobClient(FILE_NAME).upload(BinaryData.fromString(fileContent));

String response = azureStorageService.getFileLocation(FILE_NAME);

assertNotNull(response);
Assertions.assertTrue(response.contains(FILE_NAME));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,21 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;

import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.blob.specialized.BlockBlobClient;
import com.azure.storage.common.StorageSharedKeyCredential;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

@SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "BlobServiceClient is immutable and thread-safe.")
public class AzureStorageService implements StorageService {

// Consistent objects
private final BlobServiceClient blobServiceClient;
private String containerName;
private final String containerName;

public AzureStorageService(StorageServiceConfiguration configuration) {
if (!configuration.getAccountReference().isEmpty()) {

StorageSharedKeyCredential credentials = createAzureCredentials(
configuration.getAccountReference(), configuration.getAccountSecret());

String azureEndpoint = createAzureStorageEndpoint(configuration.getAccountReference());
blobServiceClient = createBlobServiceClient(azureEndpoint, credentials);
containerName = configuration.getContainerName();
} else {
blobServiceClient = null;
}
public AzureStorageService(BlobServiceClient client, StorageServiceConfiguration config) {
blobServiceClient = client;
containerName = config.getContainerName();
}

public void uploadFile(String filename, byte[] fileAsString) throws StorageException {
Expand All @@ -56,22 +45,6 @@ public String getFileLocation(String filename) {
return blobClient.getBlobUrl();
}

private StorageSharedKeyCredential createAzureCredentials(String accountName, String accountKey) {
return new StorageSharedKeyCredential(accountName, accountKey);
}

private String createAzureStorageEndpoint(String containerName) {
return String.format(Locale.ROOT, "https://%s.blob.core.windows.net", containerName);
}

private BlobServiceClient createBlobServiceClient(String endpoint, StorageSharedKeyCredential credentials) {

return new BlobServiceClientBuilder()
.endpoint(endpoint)
.credential(credentials)
.buildClient();
}

private BlobContainerClient createBlobContainerClient() {
return blobServiceClient.getBlobContainerClient(containerName);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package uk.nhs.adaptors.pss.translator.storage;

import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.common.StorageSharedKeyCredential;
import jakarta.annotation.Nonnull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

import java.util.Locale;

@Configuration
public class StorageServiceConfig {

@Bean
@ConditionalOnProperty(name = "storage.type", havingValue = "Azure")
public @Nonnull StorageService azureStorageService(StorageServiceConfiguration configuration) {
StorageSharedKeyCredential credentials = new StorageSharedKeyCredential(
configuration.getAccountReference(),
configuration.getAccountSecret()

Check warning on line 23 in gp2gp-translator/src/main/java/uk/nhs/adaptors/pss/translator/storage/StorageServiceConfig.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 23 without causing a test to fail

swapped parameters 1 and 2 in call to <init> (covered by 1 tests ParamSwapMutator)
);

String endpoint = String.format(
Locale.ROOT,
"https://%s.blob.core.windows.net",
configuration.getAccountReference()
);

BlobServiceClient blobServiceClient = new BlobServiceClientBuilder()
.endpoint(endpoint)
.credential(credentials)

Check warning on line 34 in gp2gp-translator/src/main/java/uk/nhs/adaptors/pss/translator/storage/StorageServiceConfig.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 34 without causing a test to fail

removed call to credential (covered by 1 tests RemoveChainedCallsMutator)
.buildClient();

return new AzureStorageService(blobServiceClient, configuration);
}

@Bean
@ConditionalOnProperty(name = "storage.type", havingValue = "S3")
public @Nonnull StorageService awsStorageService(StorageServiceConfiguration configuration) {
return new AWSStorageService(
S3Client.builder().build(),
configuration,
S3Presigner.builder().build()
);
}

@Bean
@ConditionalOnProperty(name = "storage.type", havingValue = "LocalMock", matchIfMissing = true)
public @Nonnull StorageService localStorageService() {
return new LocalStorageService();
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package uk.nhs.adaptors.pss.translator.storage;

import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.models.BlobProperties;
import com.azure.storage.blob.specialized.BlockBlobClient;
import com.azure.storage.blob.BlobClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class AzureStorageServiceTest {

private static final String CONTAINER_NAME = "container";
private static final String FILE_NAME = "testfile.txt";
private static final byte[] FILE_CONTENT = "mock-content".getBytes(StandardCharsets.UTF_8);

@Mock
private BlobServiceClient blobServiceClient;
@Mock
private StorageServiceConfiguration configuration;
@Mock
private BlobContainerClient blobContainerClient;
@Mock
private BlobClient blobClient;
@Mock
private BlockBlobClient blockBlobClient;
@Mock
private BlobProperties blobProperties;

private AzureStorageService azureStorageService;

@BeforeEach
void setUp() {
when(configuration.getContainerName()).thenReturn(CONTAINER_NAME);
azureStorageService = new AzureStorageService(blobServiceClient, configuration);
}

private void mockBlobClientChain() {
when(blobServiceClient.getBlobContainerClient(CONTAINER_NAME)).thenReturn(blobContainerClient);
when(blobContainerClient.getBlobClient(FILE_NAME)).thenReturn(blobClient);
when(blobClient.getBlockBlobClient()).thenReturn(blockBlobClient);
}

@Test
void When_UploadFile_Expect_SuccessfullyUploadsToAzure() throws StorageException {
mockBlobClientChain();

azureStorageService.uploadFile(FILE_NAME, FILE_CONTENT);

verify(blockBlobClient).upload(any(InputStream.class), eq((long) FILE_CONTENT.length));
}

@Test
void When_DownloadFile_Expect_SuccessfullyDownloadsFromAzure() throws StorageException {
mockBlobClientChain();
when(blockBlobClient.getProperties()).thenReturn(blobProperties);
when(blobProperties.getBlobSize()).thenReturn((long) FILE_CONTENT.length);

doAnswer(invocation -> {
ByteArrayOutputStream outputStream = invocation.getArgument(0);
outputStream.write(FILE_CONTENT);
return null;
}).when(blockBlobClient).downloadStream(any(ByteArrayOutputStream.class));

byte[] result = azureStorageService.downloadFile(FILE_NAME);

assertArrayEquals(FILE_CONTENT, result);
}

@Test
void When_DeleteFile_Expect_SuccessfullyDeletesFromAzure() {
mockBlobClientChain();

azureStorageService.deleteFile(FILE_NAME);

verify(blockBlobClient).delete();
}

@Test
void When_GetFileLocation_Expect_ReturnsCorrectUrl() {
mockBlobClientChain();
String expectedUrl = "https://azuremock.blob.core.windows.net/test-container/testfile.txt";
when(blockBlobClient.getBlobUrl()).thenReturn(expectedUrl);

String result = azureStorageService.getFileLocation(FILE_NAME);

assertEquals(expectedUrl, result);
}
}
Loading
Loading