Skip to content

Commit

Permalink
Implement delete correspondence (#135)
Browse files Browse the repository at this point in the history
* delete correspondence

* fix CorrespondenceStatus to be correct everywhere

* add tests

---------

Co-authored-by: Hammerbeck <andreas.hammerbeck@digdir.no>
  • Loading branch information
Andreass2 and Hammerbeck committed Jun 27, 2024
1 parent a0cbb5c commit c503a36
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 27 deletions.
93 changes: 81 additions & 12 deletions Test/Altinn.Correspondence.Tests/CorrespondenceControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,7 @@ public async Task ReceiverMarkActions_CorrespondenceNotPublished_ReturnBadReques
[Fact]
public async Task ReceiverMarkActions_CorrespondencePublished_ReturnOk()
{

var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();
var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var uploadedAttachment = await (await UploadAttachment(attachmentId)).Content.ReadFromJsonAsync<AttachmentOverviewExt>(_responseSerializerOptions);
var uploadedAttachment = await InitializeAttachment();
Assert.NotNull(uploadedAttachment);
var correspondence = InitializeCorrespondenceFactory.BasicCorrespondenceWithFileAttachment(uploadedAttachment.DataLocationUrl);
correspondence.VisibleFrom = DateTime.UtcNow.AddMinutes(-1);
Expand All @@ -140,15 +135,80 @@ public async Task ReceiverMarkActions_CorrespondencePublished_ReturnOk()
[Fact]
public async Task Correspondence_with_dataLocationUrl_Reuses_Attachment()
{
var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();
var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var uploadedAttachment = await (await UploadAttachment(attachmentId)).Content.ReadFromJsonAsync<AttachmentOverviewExt>(_responseSerializerOptions);
var uploadedAttachment = await InitializeAttachment();
var initializeCorrespondenceResponse = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", InitializeCorrespondenceFactory.BasicCorrespondenceWithFileAttachment(uploadedAttachment.DataLocationUrl), _responseSerializerOptions);
var response = await initializeCorrespondenceResponse.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
initializeCorrespondenceResponse.EnsureSuccessStatusCode();
Assert.Equal(attachmentId, response?.AttachmentIds?.FirstOrDefault().ToString());
Assert.Equal(uploadedAttachment.AttachmentId.ToString(), response?.AttachmentIds?.FirstOrDefault().ToString());
}

[Fact]
public async Task Delete_Initialized_Correspondence_Gives_OK()
{
var initializeCorrespondenceResponse = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", InitializeCorrespondenceFactory.BasicCorrespondence());
var correspondence = await initializeCorrespondenceResponse.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
Assert.NotNull(correspondence);
var response = await _client.DeleteAsync($"correspondence/api/v1/correspondence/{correspondence.CorrespondenceId}/purge");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var overview = await _client.GetFromJsonAsync<CorrespondenceOverviewExt>($"correspondence/api/v1/correspondence/{correspondence.CorrespondenceId}", _responseSerializerOptions);
Assert.Equal(overview?.Status, CorrespondenceStatusExt.PurgedByRecipient);
}

[Fact]
public async Task Delete_Correspondence_Also_deletes_attachment()
{
var initializeCorrespondenceResponse = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", InitializeCorrespondenceFactory.BasicCorrespondence());
var correspondence = await initializeCorrespondenceResponse.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
Assert.NotNull(correspondence);
var response = await _client.DeleteAsync($"correspondence/api/v1/correspondence/{correspondence.CorrespondenceId}/purge");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var attachment = await _client.GetFromJsonAsync<AttachmentOverviewExt>($"correspondence/api/v1/attachment/{correspondence.AttachmentIds.FirstOrDefault()}", _responseSerializerOptions);
Assert.Equal(attachment?.Status, AttachmentStatusExt.Purged);
}

[Fact]
public async Task Delete_correspondence_dont_delete_attachment_with_multiple_correspondences()
{
var attachment = await InitializeAttachment();
Assert.NotNull(attachment);
var correspondence1 = InitializeCorrespondenceFactory.BasicCorrespondenceWithFileAttachment(attachment.DataLocationUrl);
var correspondence2 = InitializeCorrespondenceFactory.BasicCorrespondenceWithFileAttachment(attachment.DataLocationUrl);

var initializeCorrespondenceResponse1 = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", correspondence1, _responseSerializerOptions);
var response1 = await initializeCorrespondenceResponse1.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
initializeCorrespondenceResponse1.EnsureSuccessStatusCode();
Assert.NotNull(response1);

var initializeCorrespondenceResponse2 = await _client.PostAsJsonAsync("correspondence/api/v1/correspondence", correspondence2, _responseSerializerOptions);
var response2 = await initializeCorrespondenceResponse2.Content.ReadFromJsonAsync<InitializeCorrespondenceResponseExt>();
initializeCorrespondenceResponse2.EnsureSuccessStatusCode();
Assert.NotNull(response2);

var deleteResponse = await _client.DeleteAsync($"correspondence/api/v1/correspondence/{response1.CorrespondenceId}/purge");
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);

var attachmentOverview = await _client.GetFromJsonAsync<AttachmentOverviewExt>($"correspondence/api/v1/attachment/{response1.AttachmentIds.FirstOrDefault()}", _responseSerializerOptions);
Assert.NotEqual(attachmentOverview?.Status, AttachmentStatusExt.Purged);
}

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

[Fact]
public async Task Delete_Published_Correspondences_As_Sender_Fails()
{
//TODO: When we implement sender
Assert.True(true);
}
[Fact]
public async Task Delete_Initialized_Correspondences_As_Receiver_Fails()
{
//TODO: When we implement Receiver
Assert.True(true);
}

private async Task<HttpResponseMessage> UploadAttachment(string? attachmentId, ByteArrayContent? originalAttachmentData = null)
Expand All @@ -162,4 +222,13 @@ private async Task<HttpResponseMessage> UploadAttachment(string? attachmentId, B
var uploadResponse = await _client.PostAsync($"correspondence/api/v1/attachment/{attachmentId}/upload", data);
return uploadResponse;
}
private async Task<AttachmentOverviewExt?> InitializeAttachment()
{
var attachment = InitializeAttachmentFactory.BasicAttachment();
var initializeResponse = await _client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
initializeResponse.EnsureSuccessStatusCode();
var attachmentId = await initializeResponse.Content.ReadAsStringAsync();
var overview = await (await UploadAttachment(attachmentId)).Content.ReadFromJsonAsync<AttachmentOverviewExt>(_responseSerializerOptions);
return overview;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Altinn.Correspondence.Application.GetCorrespondenceOverview;
using Altinn.Correspondence.Application.GetCorrespondences;
using Altinn.Correspondence.Application.InitializeCorrespondence;
using Altinn.Correspondence.Application.PurgeCorrespondence;
using Altinn.Correspondence.Application.UpdateCorrespondenceStatus;
using Altinn.Correspondence.Core.Models.Enums;
using Altinn.Correspondence.Helpers;
Expand Down Expand Up @@ -261,13 +262,20 @@ public async Task<ActionResult> Archive(
/// </remarks>
/// <returns>Ok</returns>
[HttpDelete]
[Route("{correspondenceId}/delete")]
public async Task<ActionResult> Delete(
Guid correspondenceId)
[Route("{correspondenceId}/purge")]
public async Task<ActionResult> Purge(
Guid correspondenceId,
[FromServices] PurgeCorrespondenceHandler handler,
CancellationToken cancellationToken)
{
_logger.LogInformation("Deleting Correspondence with id: {correspondenceId}", correspondenceId.ToString());
_logger.LogInformation("Purging Correspondence with id: {correspondenceId}", correspondenceId.ToString());

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

return Ok();
return commandResult.Match(
data => Ok(data),
Problem
);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public enum CorrespondenceStatusExt : int
/// <summary>
/// Message has been purged by recipient.
/// </summary>
PurgedBByRecipient = 7,
PurgedByRecipient = 7,
/// <summary>
/// Message has been purged by Altinn.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Altinn.Correspondence.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Altinn.Correspondence.Application.InitializeAttachment;
using Altinn.Correspondence.Application.InitializeCorrespondence;
using Altinn.Correspondence.Application.PurgeAttachment;
using Altinn.Correspondence.Application.PurgeCorrespondence;
using Altinn.Correspondence.Application.UpdateCorrespondenceStatus;
using Altinn.Correspondence.Application.UploadAttachment;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -29,5 +30,6 @@ public static void AddApplicationHandlers(this IServiceCollection services)
services.AddScoped<DownloadAttachmentHandler>();
services.AddScoped<PurgeAttachmentHandler>();
services.AddScoped<MalwareScanResultHandler>();
services.AddScoped<PurgeCorrespondenceHandler>();
}
}
1 change: 1 addition & 0 deletions src/Altinn.Correspondence.Application/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ public static class Errors
public static Error NoMessageBody = new Error(11, "Atleast one attachment must be marked as message body", HttpStatusCode.BadRequest);
public static Error NoAttachments = new Error(12, "Need atleast one attachments, which must be marked as message body", HttpStatusCode.BadRequest);
public static Error CorrespondencePurged = new Error(13, "Correspondence has been purged", HttpStatusCode.BadRequest);
public static Error CorrespondenceAlreadyPurged = new Error(14, "Correspondence has already been purged", HttpStatusCode.BadRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public CorrespondenceStatus GetInitializeCorrespondenceStatus(CorrespondenceEnti
public async Task<AttachmentEntity> ProcessAttachment(CorrespondenceAttachmentEntity correspondenceAttachment, CancellationToken cancellationToken)
{
AttachmentEntity? attachment = null;
if (correspondenceAttachment.DataLocationUrl != null)
if (!String.IsNullOrEmpty(correspondenceAttachment.DataLocationUrl))
{
var existingAttachment = await _attachmentRepository.GetAttachmentByUrl(correspondenceAttachment.DataLocationUrl, cancellationToken);
if (existingAttachment != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Altinn.Correspondence.Core.Models;
using Altinn.Correspondence.Core.Models.Enums;
using Altinn.Correspondence.Core.Repositories;
using OneOf;

namespace Altinn.Correspondence.Application.PurgeCorrespondence;

public class PurgeCorrespondenceHandler : IHandler<Guid, Guid>
{
private readonly IAttachmentRepository _attachmentRepository;
private readonly IAttachmentStatusRepository _attachmentStatusRepository;
private readonly ICorrespondenceRepository _correspondenceRepository;
private readonly ICorrespondenceStatusRepository _correspondenceStatusRepository;
private readonly IStorageRepository _storageRepository;

public PurgeCorrespondenceHandler(IAttachmentRepository attachmentRepository, ICorrespondenceRepository correspondenceRepository, ICorrespondenceStatusRepository correspondenceStatusRepository, IStorageRepository storageRepository, IAttachmentStatusRepository attachmentStatusRepository)
{
_attachmentRepository = attachmentRepository;
_correspondenceRepository = correspondenceRepository;
_correspondenceStatusRepository = correspondenceStatusRepository;
_storageRepository = storageRepository;
_attachmentStatusRepository = attachmentStatusRepository;
}

public async Task<OneOf<Guid, Error>> Process(Guid correspondenceId, CancellationToken cancellationToken)
{
var correspondence = await _correspondenceRepository.GetCorrespondenceById(correspondenceId, true, false, cancellationToken);
if (correspondence == null) return Errors.AttachmentNotFound;

if (correspondence.Statuses.Any(status => status.Status == CorrespondenceStatus.PurgedByRecipient || status.Status == CorrespondenceStatus.PurgedByAltinn))
{
return Errors.CorrespondenceAlreadyPurged;
}

// TODO: sender should only be able to delete correspondence if it is not published
// Receiver should be able to delete correspondence if it is published

var newStatus = new CorrespondenceStatusEntity()
{
CorrespondenceId = correspondenceId,
Status = CorrespondenceStatus.PurgedByRecipient, // Todo: select status based on user role
StatusChanged = DateTimeOffset.UtcNow,
StatusText = CorrespondenceStatus.PurgedByRecipient.ToString()
};
await _correspondenceStatusRepository.AddCorrespondenceStatus(newStatus, cancellationToken);
await CheckAndPurgeAttachments(correspondenceId, cancellationToken);
return correspondenceId;
}

public async Task CheckAndPurgeAttachments(Guid correspondenceId, CancellationToken cancellationToken)
{
var attachments = await _attachmentRepository.GetAttachmentsByCorrespondence(correspondenceId, cancellationToken);
foreach (var attachment in attachments)
{
var canBeDeleted = await _attachmentRepository.CanAttachmentBeDeleted(attachment.Id, cancellationToken);
if (!canBeDeleted || attachment.Statuses.Any(status => status.Status == AttachmentStatus.Purged))
{
continue;
}

await _storageRepository.PurgeAttachment(attachment.Id, cancellationToken);
var attachmentStatus = new AttachmentStatusEntity
{
AttachmentId = attachment.Id,
Status = AttachmentStatus.Purged,
StatusChanged = DateTimeOffset.UtcNow,
StatusText = AttachmentStatus.Purged.ToString()
};
await _attachmentStatusRepository.AddAttachmentStatus(attachmentStatus, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,49 @@ public enum CorrespondenceStatus : int
/// </summary>
Published = 2,

/// <summary>
/// Message fetched by recipient.
/// </summary>
Fetched = 3,

/// <summary>
/// Message read by recipient.
/// </summary>
Read = 3,
Read = 4,

/// <summary>
/// Recipient has replied on message.
/// </summary>
Replied = 4,
Replied = 5,

/// <summary>
/// Message confirmed by recipient.
/// </summary>
Confirmed = 5,
Confirmed = 6,

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

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

/// <summary>
/// Message has been Archived
/// </summary>
Archived = 8,
Archived = 9,

/// <summary>
/// Recipient has opted out of digital communication in KRR
/// </summary>
Reserved = 9,
Reserved = 10,

/// <summary>
/// Message has Failed
/// </summary>
Failed = 10
Failed = 11
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public interface IAttachmentRepository
Task<AttachmentEntity?> GetAttachmentByUrl(string url, CancellationToken cancellationToken);
Task<AttachmentEntity?> GetAttachmentById(Guid attachmentId, bool includeStatus = false, CancellationToken cancellationToken = default);
Task<bool> SetDataLocationUrl(AttachmentEntity attachmentEntity, AttachmentDataLocationType attachmentDataLocationType, string dataLocationUrl, CancellationToken cancellationToken);
Task<bool> CanAttachmentBeDeleted(Guid attachmentId, CancellationToken cancellationToken);
Task<List<AttachmentEntity>> GetAttachmentsByCorrespondence(Guid correspondenceId, CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,15 @@ public async Task<bool> SetDataLocationUrl(AttachmentEntity attachmentEntity, At
var rowsUpdated = await _context.SaveChangesAsync(cancellationToken);
return rowsUpdated > 0;
}

public async Task<bool> CanAttachmentBeDeleted(Guid attachmentId, CancellationToken cancellationToken)
{
return !(await _context.Correspondences.AnyAsync(a => a.Content != null && a.Content.Attachments.Any(ca => ca.AttachmentId == attachmentId) &&
!a.Statuses.Any(s => s.Status == CorrespondenceStatus.PurgedByRecipient || s.Status == CorrespondenceStatus.PurgedByAltinn), cancellationToken));
}
public async Task<List<AttachmentEntity?>> GetAttachmentsByCorrespondence(Guid correspondenceId, CancellationToken cancellationToken)

Check warning on line 54 in src/Altinn.Correspondence.Persistence/Repositories/AttachmentRepository.cs

View workflow job for this annotation

GitHub Actions / QA / Test application

Nullability of reference types in return type of 'Task<List<AttachmentEntity?>> AttachmentRepository.GetAttachmentsByCorrespondence(Guid correspondenceId, CancellationToken cancellationToken)' doesn't match implicitly implemented member 'Task<List<AttachmentEntity>> IAttachmentRepository.GetAttachmentsByCorrespondence(Guid correspondenceId, CancellationToken cancellationToken)'.
{
return await _context.Correspondences.Where(c => c.Id == correspondenceId && c.Content != null).SelectMany(c => c.Content!.Attachments).Select(ca => ca.Attachment).ToListAsync(cancellationToken);
}
}
}

0 comments on commit c503a36

Please sign in to comment.