Skip to content

Commit

Permalink
Implement DeleteAttachment command (#102)
Browse files Browse the repository at this point in the history
* add DeleteAttachment command

* cleanup

* return no content from delete to replicate altinn 2

* fix Broker references

---------

Co-authored-by: Hammerbeck <[email protected]>
  • Loading branch information
Andreass2 and Hammerbeck authored Jun 10, 2024
1 parent 69cc9fb commit 29d8bae
Show file tree
Hide file tree
Showing 22 changed files with 350 additions and 63 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The services required to support local development are run using docker compose:
```docker compose up -d```

To support features like hot reload etc, the app itself is run directly. Either in IDE like Visual Studio or by running:
```dotnet watch --project ./src/Altinn.Broker.API/Altinn.Broker.API.csproj```
```dotnet watch --project ./src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj```

### Adding migrations:
To add a new migration you can run the following command:
Expand Down
105 changes: 88 additions & 17 deletions Test/Altinn.Correspondence.Tests/AttachmentControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Altinn.Correspondece.Tests.Factories;
using Altinn.Correspondence.API.Models;
using Altinn.Correspondence.API.Models.Enums;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Net.Http.Json;
using System.Security.Policy;
using System.Text.Json;

namespace Altinn.Correspondence.Tests;
Expand All @@ -10,11 +13,17 @@ public class AttachmentControllerTests : IClassFixture<CustomWebApplicationFacto
{
private readonly CustomWebApplicationFactory _factory;
private readonly HttpClient _client;
private readonly JsonSerializerOptions _responseSerializerOptions;

public AttachmentControllerTests(CustomWebApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClientInternal();
_responseSerializerOptions = new JsonSerializerOptions(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
});
_responseSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
}

[Fact]
Expand Down Expand Up @@ -46,11 +55,7 @@ public async Task GetAttachmentDetails()
[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);

var uploadResponse = await UploadAttachment("00000000-0100-0000-0000-000000000000");
Assert.Equal(HttpStatusCode.NotFound, uploadResponse.StatusCode);
}

Expand All @@ -62,10 +67,7 @@ public async Task UploadAttachmentData_WhenAttachmentExists_Succeeds()
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);
var uploadResponse = await UploadAttachment(attachmentId);

Assert.True(uploadResponse.IsSuccessStatusCode, await uploadResponse.Content.ReadAsStringAsync());
}
Expand All @@ -80,14 +82,12 @@ public async Task UploadAttachmentData_UploadsTwice_FailsSecondAttempt()
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);
var firstUploadResponse = await UploadAttachment(attachmentId, 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);
var secondUploadResponse = await UploadAttachment(attachmentId, content);

Assert.False(secondUploadResponse.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.BadRequest, secondUploadResponse.StatusCode);
Expand Down Expand Up @@ -121,10 +121,7 @@ public async Task UploadAttachmentData_Succeeds_DownloadedBytesAreSame()
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();
await UploadAttachment(attachmentId, content);

// Download the attachment data
var downloadResponse = await _client.GetAsync($"correspondence/api/v1/attachment/{attachmentId}/download");
Expand All @@ -135,4 +132,78 @@ public async Task UploadAttachmentData_Succeeds_DownloadedBytesAreSame()
// Assert that the uploaded and downloaded bytes are the same
Assert.Equal(originalAttachmentData, downloadedAttachmentData);
}

[Fact]
public async Task DeleteAttachment_WhenAttachmentDoesNotExist_ReturnsNotFound()
{
var deleteResponse = await _client.DeleteAsync("correspondence/api/v1/attachment/00000000-0100-0000-0000-000000000000");
Assert.Equal(HttpStatusCode.NotFound, deleteResponse.StatusCode);
}

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

var attachmentId = await initializeResponse.Content.ReadAsStringAsync();

var deleteResponse = await _client.DeleteAsync($"correspondence/api/v1/attachment/{attachmentId}");
Assert.True(deleteResponse.IsSuccessStatusCode, await deleteResponse.Content.ReadAsStringAsync());
}

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

var attachmentId = await initializeResponse.Content.ReadAsStringAsync();

var deleteResponse = await _client.DeleteAsync($"correspondence/api/v1/attachment/{attachmentId}");
Assert.True(deleteResponse.IsSuccessStatusCode, await deleteResponse.Content.ReadAsStringAsync());

var deleteResponse2 = await _client.DeleteAsync($"correspondence/api/v1/attachment/{attachmentId}");
Assert.Equal(HttpStatusCode.BadRequest, deleteResponse2.StatusCode);
}

[Fact]
public async Task DeleteAttachment_WhenAttachedCorrespondenceIsPublished_ReturnsBadRequest()
{
var initializeCorrespondenceResponse = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", InitializeCorrespondenceFactory.BasicCorrespondence());
var correspondence = await initializeCorrespondenceResponse.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
await UploadAttachment(correspondence?.AttachmentIds.First().ToString());
var overview = await _client.GetFromJsonAsync<CorrespondenceOverviewExt>($"correspondence/api/v1/correspondence/{correspondence?.CorrespondenceId}", _responseSerializerOptions);
Assert.True(overview?.Status == CorrespondenceStatusExt.Published);

var deleteResponse = await _client.DeleteAsync($"correspondence/api/v1/attachment/{correspondence?.AttachmentIds.FirstOrDefault()}");
Assert.Equal(HttpStatusCode.BadRequest, deleteResponse.StatusCode);
}
[Fact]
public async Task DeleteAttachment_WhenAttachedCorrespondenceIsInitialized_Succeeds()
{
var initializeCorrespondenceResponse = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", InitializeCorrespondenceFactory.BasicCorrespondenceWithMultipleAttachments());
var correspondence = await initializeCorrespondenceResponse.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
await UploadAttachment(correspondence?.AttachmentIds.First().ToString());
var overview = await _client.GetFromJsonAsync<CorrespondenceOverviewExt>($"correspondence/api/v1/correspondence/{correspondence?.CorrespondenceId}", _responseSerializerOptions);
Assert.True(overview?.Status == CorrespondenceStatusExt.Initialized);

var deleteResponse = await _client.DeleteAsync($"correspondence/api/v1/attachment/{correspondence?.AttachmentIds.FirstOrDefault()}");
deleteResponse.EnsureSuccessStatusCode();
}

private async Task<HttpResponseMessage> UploadAttachment(string? attachmentId, ByteArrayContent? originalAttachmentData = null)
{
if (attachmentId == null)
{
Assert.Fail("AttachmentId is null");
}
var data = originalAttachmentData ?? new ByteArrayContent(new byte[] { 1, 2, 3, 4 });

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,81 @@ internal static class InitializeCorrespondenceFactory
},
IsReservable = true
};

internal static InitializeCorrespondenceExt BasicCorrespondenceWithMultipleAttachments() => new InitializeCorrespondenceExt()
{
Recipient = "1",
ResourceId = "1",
Sender = "8536:031145332",
SendersReference = "1",
Content = new InitializeCorrespondenceContentExt()
{
Language = "no",
MessageTitle = "test",
MessageSummary = "test",
Attachments = new List<InitializeCorrespondenceAttachmentExt>() {
new InitializeCorrespondenceAttachmentExt()
{
DataType = "html",
Name = "2",
RestrictionName = "testFile2",
SendersReference = "1234",
IntendedPresentation = IntendedPresentationTypeExt.HumanReadable,
FileName = "test-fil2e",
IsEncrypted = false,
},
new InitializeCorrespondenceAttachmentExt()
{
DataType = "html",
Name = "3",
RestrictionName = "testFile3",
SendersReference = "1234",
IntendedPresentation = IntendedPresentationTypeExt.HumanReadable,
FileName = "test-fil3e",
IsEncrypted = false,
},
},
},
VisibleFrom = DateTime.UtcNow,
AllowSystemDeleteAfter = DateTime.UtcNow.AddDays(1),
DueDateTime = DateTime.UtcNow.AddDays(1),
ExternalReferences = new List<ExternalReferenceExt>(){
new ExternalReferenceExt()
{
ReferenceValue = "1",
ReferenceType = ReferenceTypeExt.AltinnBrokerFileTransfer
},
new ExternalReferenceExt()
{
ReferenceValue = "2",
ReferenceType = ReferenceTypeExt.DialogPortenDialogID
}
},
PropertyList = new Dictionary<string, string>(){
{"deserunt_12", "1"},
{"culpa_852", "2"},
{"anim5", "3"}
},
ReplyOptions = new List<CorrespondenceReplyOptionExt>(){
new CorrespondenceReplyOptionExt()
{
LinkURL = "www.test.no",
LinkText = "test"
},
new CorrespondenceReplyOptionExt()
{
LinkURL = "test.no",
LinkText = "test"
}
},
Notifications = new List<InitializeCorrespondenceNotificationExt>(){
new InitializeCorrespondenceNotificationExt(){
NotificationTemplate= "test",
CustomTextToken = "test",
SendersReference = "1",
RequestedSendTime = DateTime.UtcNow.AddDays(1),
}
},
IsReservable = true
};
}
6 changes: 3 additions & 3 deletions altinn-correspondence-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@
"Initialized",
"UploadProcessing",
"Published",
"Deleted",
"Purged",
"Failed"
],
"type": "string"
Expand Down Expand Up @@ -1077,8 +1077,8 @@
"Read",
"Replied",
"Confirmed",
"DeletedByRecipient",
"DeletedByAltinn",
"PurgedByRecipient",
"PurgedByAltinn",
"Archived",
"Reserved",
"Failed"
Expand Down
26 changes: 12 additions & 14 deletions src/Altinn.Correspondence.API/Controllers/AttachmentController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Altinn.Correspondence.API.Models;
using Altinn.Correspondence.API.Models.Enums;
using Altinn.Correspondence.Application;
using Altinn.Correspondence.Application.PurgeAttachmentCommand;
using Altinn.Correspondence.Application.DownloadAttachmentQuery;
using Altinn.Correspondence.Application.GetAttachmentDetailsCommand;
using Altinn.Correspondence.Application.GetAttachmentOverviewCommand;
Expand Down Expand Up @@ -145,22 +146,19 @@ public async Task<ActionResult> DownloadAttachmentData(
/// <returns></returns>
[HttpDelete]
[Route("{attachmentId}")]
[Authorize]
public async Task<ActionResult<AttachmentOverviewExt>> DeleteAttachment(
Guid attachmentId)
Guid attachmentId,
[FromServices] PurgeAttachmentCommandHandler handler,
CancellationToken cancellationToken)
{
// Should this just give back HTTP Status codes?
return new AttachmentOverviewExt
{
AttachmentId = attachmentId,
Name = "TestName",
SendersReference = "1234",
DataType = "application/pdf",
IntendedPresentation = IntendedPresentationTypeExt.HumanReadable,
Status = AttachmentStatusExt.Deleted,
StatusText = "Attachment is deleted",
StatusChanged = DateTimeOffset.UtcNow
};
_logger.LogInformation("Delete attachment {attachmentId}", attachmentId.ToString());

var commandResult = await handler.Process(attachmentId, cancellationToken);

return commandResult.Match(
_ => Ok(null),
Problem
);
}
private ActionResult Problem(Error error) => Problem(detail: error.Message, statusCode: (int)error.StatusCode);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Altinn.Broker.Application;
using Altinn.Correspondence.Application;
using Altinn.Correspondence.Application;
using Altinn.Correspondence.Application.MalwareScanResultCommand.Models;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ public enum AttachmentStatusExt
Published = 2,

/// <summary>
/// Deleted
/// Purged
/// </summary>
Deleted = 3,
Purged = 3,

/// <summary>
/// Failed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public enum CorrespondenceStatusExt : int
Confirmed = 5,

/// <summary>
/// Message has been deleted by recipient.
/// Message has been purged by recipient.
/// </summary>
DeletedByRecipient = 6,
PurgedByRecipient = 6,

/// <summary>
/// Message has been deleted by Altinn.
/// Message has been purged by Altinn.
/// </summary>
DeletedByAltinn = 7,
PurgedByAltinn = 7,

/// <summary>
/// Message has been Archived
Expand Down
3 changes: 2 additions & 1 deletion src/Altinn.Correspondence.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using Altinn.Correspondence.Application.UploadAttachmentCommand;
using Altinn.Correspondence.Application.UpdateCorrespondenceStatusCommand;
using Microsoft.Extensions.DependencyInjection;
using Altinn.Broker.Application;
using Altinn.Correspondence.Application.PurgeAttachmentCommand;

namespace Altinn.Correspondence.Application;
public static class DependencyInjection
Expand All @@ -26,6 +26,7 @@ public static void AddApplicationHandlers(this IServiceCollection services)
services.AddScoped<UpdateCorrespondenceStatusCommandHandler>();
services.AddScoped<UploadAttachmentCommandHandler>();
services.AddScoped<DownloadAttachmentQueryHandler>();
services.AddScoped<PurgeAttachmentCommandHandler>();
services.AddScoped<MalwareScanResultCommandHandler>();
}
}
6 changes: 4 additions & 2 deletions src/Altinn.Correspondence.Application/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public static class Errors
public static Error OffsetAndLimitIsNegative = new(3, "Limit and offset must be greater than or equal to 0", HttpStatusCode.BadRequest);
public static Error UploadFailed = new(4, "Error occurred during upload", HttpStatusCode.BadGateway);
public static Error InvalidFileSize = new(5, "File must have content and has a max file size of 2GB", HttpStatusCode.BadRequest);
public static Error InvalidAttachmentStatus = new(6, "File has already been or is being uploaded", HttpStatusCode.BadRequest);
public static Error CorrespondenceNotPublished = new Error(7, "A correspondence can only be confirmed or read when it is published. See correspondence status.", HttpStatusCode.BadRequest);
public static Error InvalidUploadAttachmentStatus = new(6, "File has already been or is being uploaded", HttpStatusCode.BadRequest);
public static Error InvalidPurgeAttachmentStatus = new(7, "File has already been purged", HttpStatusCode.BadRequest);
public static Error CorrespondenceNotPublished = new Error(8, "A correspondence can only be confirmed or read when it is published. See correspondence status.", HttpStatusCode.BadRequest);
public static Error PurgeAttachmentWithExistingCorrespondence = new Error(9, "Attachment cannot be purged as it is linked to atleast one existing correspondence", HttpStatusCode.BadRequest);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using Altinn.Correspondence.Application;
using Altinn.Correspondence.Application.MalwareScanResultCommand.Models;
using Altinn.Correspondence.Application.MalwareScanResultCommand.Models;
using Altinn.Correspondence.Core.Models.Enums;
using Altinn.Correspondence.Core.Repositories;
using Microsoft.Extensions.Logging;
using OneOf;
using System.Text.Json;

namespace Altinn.Broker.Application;
namespace Altinn.Correspondence.Application;
public class MalwareScanResultCommandHandler(
IAttachmentRepository attachmentRepository,
IAttachmentStatusRepository attachmentStatusRepository,
Expand Down
Loading

0 comments on commit 29d8bae

Please sign in to comment.