Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attachment backend #79

Merged
merged 12 commits into from
May 30, 2024
8 changes: 4 additions & 4 deletions .azure/infrastructure/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ module postgresql '../modules/postgreSql/create.bicep' = {
}
}

module migrationsStorageAccount '../modules/storageAccount/create.bicep' = {
module storageAccount '../modules/storageAccount/create.bicep' = {
scope: resourceGroup
name: migrationsStorageAccountName
params: {
migrationsStorageAccountName: migrationsStorageAccountName
storageAccountName: migrationsStorageAccountName
location: location
fileshare: 'migrations'
}
Expand All @@ -87,12 +87,12 @@ module migrationsStorageAccount '../modules/storageAccount/create.bicep' = {
module containerAppEnv '../modules/containerAppEnvironment/main.bicep' = {
scope: resourceGroup
name: 'container-app-environment'
dependsOn: [migrationsStorageAccount]
dependsOn: [storageAccount]
params: {
keyVaultName: sourceKeyVaultName
location: location
namePrefix: namePrefix
migrationsStorageAccountName: migrationsStorageAccountName
storageAccountName: migrationsStorageAccountName
}
}
output resourceGroupName string = resourceGroup.name
Expand Down
6 changes: 6 additions & 0 deletions .azure/modules/containerApp/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var containerAppEnvVars = [
{ name: 'ASPNETCORE_ENVIRONMENT', value: environment }
{ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', secretRef: 'application-insights-connection-string' }
{ name: 'DatabaseOptions__ConnectionString', secretRef: 'correspondence-ado-connection-string' }
{ name: 'AttachmentStorageOptions__ConnectionString', secretRef: 'storage-account-key'}
{ name: 'AzureResourceManagerOptions__SubscriptionId', value: subscription_id }
{ name: 'AzureResourceManagerOptions__Location', value: 'norwayeast' }
{ name: 'AzureResourceManagerOptions__Environment', value: environment }
Expand Down Expand Up @@ -69,6 +70,11 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
keyVaultUrl: '${keyVaultUrl}/secrets/correspondence-ado-connection-string'
name: 'correspondence-ado-connection-string'
}
{
identity: principal_id
keyVaultUrl: '${keyVaultUrl}/secrets/storage-account-key'
name: 'storage-account-key'
}
]
}

Expand Down
16 changes: 13 additions & 3 deletions .azure/modules/containerAppEnvironment/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ param location string
param namePrefix string
@secure()
param keyVaultName string
param migrationsStorageAccountName string
param storageAccountName string

resource log_analytics_workspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: '${namePrefix}-log'
Expand Down Expand Up @@ -41,7 +41,7 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2023-11-02-p
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
name: migrationsStorageAccountName
name: storageAccountName
}

resource containerAppEnvironmentStorage 'Microsoft.App/managedEnvironments/storages@2023-11-02-preview' = {
Expand All @@ -51,7 +51,7 @@ resource containerAppEnvironmentStorage 'Microsoft.App/managedEnvironments/stora
azureFile: {
accessMode: 'ReadOnly'
accountKey: storageAccount.listKeys().keys[0].value
accountName: migrationsStorageAccountName
accountName: storageAccountName
shareName: 'migrations'
}
}
Expand All @@ -77,4 +77,14 @@ module containerAppEnvIdSecret '../keyvault/upsertSecret.bicep' = {
}
}

var storageAccountKeySecretName = 'storage-account-key'
module storageAccountKeySecret '../keyvault/upsertSecret.bicep' = {
name: storageAccountKeySecretName
params: {
destKeyVaultName: keyVaultName
secretName: storageAccountKeySecretName
secretValue: storageAccount.listKeys().keys[0].value
}
}

output containerAppEnvironmentId string = containerAppEnvironment.id
15 changes: 12 additions & 3 deletions .azure/modules/storageAccount/create.bicep
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
@secure()
param migrationsStorageAccountName string
param storageAccountName string
param fileshare string
param location string

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: migrationsStorageAccountName
name: storageAccountName
location: location
kind: 'StorageV2'
sku: {
Expand All @@ -20,10 +20,19 @@ resource storageAccountFileServices 'Microsoft.Storage/storageAccounts/fileServi
parent: storageAccount
}


resource storageAccountFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-01-01' = {
name: fileshare
parent: storageAccountFileServices
}

resource storageAccountBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-04-01' = {
name: 'default'
parent: storageAccount
}

resource storageAccountAttachmentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-04-01' = {
name: 'attachments'
parent: storageAccountBlobServices
}

output storageAccountId string = storageAccount.id
98 changes: 97 additions & 1 deletion Test/Altinn.Correspondence.Tests/AttachmentControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Net.Http.Json;
using Altinn.Correspondece.Tests.Factories;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;

namespace Altinn.Correspondence.Tests;

Expand Down Expand Up @@ -39,4 +42,97 @@ public async Task GetAttachmentDetails()
var getAttachmentOverviewResponse = await _client.GetAsync($"correspondence/api/v1/attachment/{attachmentId}/details");
Assert.True(getAttachmentOverviewResponse.IsSuccessStatusCode, await getAttachmentOverviewResponse.Content.ReadAsStringAsync());
}

[Fact]
public async Task UploadAttachmentData_WhenAttachmentDoesNotExist_ReturnsNotFound()
{
var attachmentData = new byte[] { 1, 2, 3, 4 };
var content = new ByteArrayContent(attachmentData);

var uploadResponse = await _client.PostAsync("correspondence/api/v1/attachment/00000000-0100-0000-0000-000000000000/upload", content);

Assert.Equal(HttpStatusCode.NotFound, uploadResponse.StatusCode);
}

[Fact]
public async Task UploadAttachmentData_WhenAttachmentExists_Succeeds()
{
var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();

var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var attachmentData = new byte[] { 1, 2, 3, 4 };
var content = new ByteArrayContent(attachmentData);

var uploadResponse = await _client.PostAsync($"correspondence/api/v1/attachment/{attachmentId}/upload", content);

Assert.True(uploadResponse.IsSuccessStatusCode, await uploadResponse.Content.ReadAsStringAsync());
}

[Fact]
public async Task UploadAttachmentData_UploadsTwice_FailsSecondAttempt()
{
var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();

var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var attachmentData = new byte[] { 1, 2, 3, 4 };
var content = new ByteArrayContent(attachmentData);

// First upload
var firstUploadResponse = await _client.PostAsync($"correspondence/api/v1/attachment/{attachmentId}/upload", content);
Assert.True(firstUploadResponse.IsSuccessStatusCode, await firstUploadResponse.Content.ReadAsStringAsync());

// Second upload
content = new ByteArrayContent(attachmentData);
var secondUploadResponse = await _client.PostAsync($"correspondence/api/v1/attachment/{attachmentId}/upload", content);

Assert.False(secondUploadResponse.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.BadRequest, secondUploadResponse.StatusCode);
}

[Fact]
public async Task UploadAttachmentData_UploadFails_GetErrorMessage()
{
var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();

var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var content = new StreamContent(new MemoryStream([])); // Empty content to simulate failure

var uploadResponse = await _client.PostAsync($"correspondence/api/v1/attachment/{attachmentId}/upload", content);

Assert.False(uploadResponse.IsSuccessStatusCode);
var errorMessage = await uploadResponse.Content.ReadAsStringAsync();
var error = JsonSerializer.Deserialize<ProblemDetails>(errorMessage);
Assert.NotNull(error);
}

[Fact]
public async Task UploadAttachmentData_Succeeds_DownloadedBytesAreSame()
{
var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();

var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var originalAttachmentData = new byte[] { 1, 2, 3, 4 };
var content = new ByteArrayContent(originalAttachmentData);

// Upload the attachment data
var uploadResponse = await _client.PostAsync($"correspondence/api/v1/attachment/{attachmentId}/upload", content);
uploadResponse.EnsureSuccessStatusCode();

// Download the attachment data
var downloadResponse = await _client.GetAsync($"correspondence/api/v1/attachment/{attachmentId}/download");
downloadResponse.EnsureSuccessStatusCode();

var downloadedAttachmentData = await downloadResponse.Content.ReadAsByteArrayAsync();

// Assert that the uploaded and downloaded bytes are the same
Assert.Equal(originalAttachmentData, downloadedAttachmentData);
}
}
28 changes: 22 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,28 @@ services:
image: 'postgres:latest'
ports:
- 5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 1s
timeout: 30s
retries: 30
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: correspondence
POSTGRES_DB: correspondence
storage:
image: mcr.microsoft.com/azure-storage/azurite:latest
ports:
- "10000:10000"
- "10001:10001"
healthcheck:
test: nc 127.0.0.1 10000 -z
interval: 1s
retries: 30
storage_init:
image: mcr.microsoft.com/azure-cli:latest
command:
- /bin/sh
- -c
- |
az storage container create --name attachments
depends_on:
storage:
condition: service_healthy
environment:
AZURE_STORAGE_CONNECTION_STRING: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://storage:10000/devstoreaccount1;
Loading
Loading