Skip to content

Commit

Permalink
Merge pull request #30 from dlcs/feature/deleteStorageCollection
Browse files Browse the repository at this point in the history
Adding delete collection
  • Loading branch information
JackLewis-digirati authored Sep 19, 2024
2 parents b1d2ad0 + 0ca2256 commit 12f3c17
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 40 deletions.
110 changes: 110 additions & 0 deletions src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,4 +464,114 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenCalledWithoutNeed
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}

[Fact]
public async Task DeleteCollection_DeletesCollection_WhenAllValuesProvided()
{
// Arrange
var initialCollection = new Collection()
{
Id = "DeleteTester",
Slug = "delete-test",
UsePath = true,
Label = new LanguageMap
{
{ "en", new List<string> { "update testing" } }
},
Thumbnail = "some/location",
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
CreatedBy = "admin",
Tags = "some, tags",
IsStorageCollection = true,
IsPublic = false,
CustomerId = 1,
Parent = "RootStorage"
};

await dbContext.Collections.AddAsync(initialCollection);
await dbContext.SaveChangesAsync();


var deleteRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Delete,
$"{Customer}/collections/{initialCollection.Id}");

// Act
var response = await httpClient.AsCustomer(1).SendAsync(deleteRequestMessage);

var fromDatabase = dbContext.Collections.FirstOrDefault(c => c.Id == initialCollection.Id);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
fromDatabase.Should().BeNull();
}

[Fact]
public async Task DeleteCollection_FailsToDeleteCollection_WhenNotFound()
{
// Arrange
var deleteRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Delete,
$"{Customer}/collections/doesNotExist");

// Act
var response = await httpClient.AsCustomer(1).SendAsync(deleteRequestMessage);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

[Fact]
public async Task DeleteCollection_FailsToDeleteCollection_WhenAttemptingToDeleteRoot()
{
// Arrange
var deleteRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Delete,
$"{Customer}/collections/root");

// Act
var response = await httpClient.AsCustomer(1).SendAsync(deleteRequestMessage);

var errorResponse = await response.ReadAsPresentationResponseAsync<Error>();

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
errorResponse!.ErrorTypeUri.Should().Be("http://localhost/errors/DeleteCollectionType/CannotDeleteRootCollection");
errorResponse.Detail.Should().Be("Cannot delete a root collection");
}


[Fact]
public async Task DeleteCollection_FailsToDeleteCollection_WhenAttemptingToDeleteRootDirectly()
{
// Arrange
var deleteRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Delete,
$"{Customer}/collections/RootStorage");

// Act
var response = await httpClient.AsCustomer(1).SendAsync(deleteRequestMessage);

var errorResponse = await response.ReadAsPresentationResponseAsync<Error>();

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
errorResponse!.ErrorTypeUri.Should().Be("http://localhost/errors/DeleteCollectionType/CannotDeleteRootCollection");
errorResponse.Detail.Should().Be("Cannot delete a root collection");
}

[Fact]
public async Task DeleteCollection_FailsToDeleteCollection_WhenAttemptingToDeleteCollectionWithItems()
{
// Arrange
var deleteRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Delete,
$"{Customer}/collections/FirstChildCollection");

// Act
var response = await httpClient.AsCustomer(1).SendAsync(deleteRequestMessage);

var errorResponse = await response.ReadAsPresentationResponseAsync<Error>();

// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
errorResponse!.ErrorTypeUri.Should().Be("http://localhost/errors/DeleteCollectionType/CollectionNotEmpty");
errorResponse.Detail.Should().Be("Cannot delete a collection with child items");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Core;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Models.API.General;
using Repository;

namespace API.Features.Storage.Requests;

public class DeleteCollection (int customerId, string collectionId) : IRequest<ResultMessage<DeleteResult, DeleteCollectionType>>
{
public int CustomerId { get; } = customerId;

public string CollectionId { get; } = collectionId;
}

public class DeleteCollectionHandler(
PresentationContext dbContext,
ILogger<CreateCollection> logger)
: IRequestHandler<DeleteCollection, ResultMessage<DeleteResult, DeleteCollectionType>>
{
private const string RootCollection = "root";

public async Task<ResultMessage<DeleteResult, DeleteCollectionType>> Handle(DeleteCollection request, CancellationToken cancellationToken)
{
logger.LogDebug("Deleting collection {CollectionId} for customer {CustomerId}", request.CollectionId,
request.CustomerId);

if (request.CollectionId.Equals(RootCollection, StringComparison.OrdinalIgnoreCase))
{
return new ResultMessage<DeleteResult, DeleteCollectionType>(DeleteResult.BadRequest,
DeleteCollectionType.CannotDeleteRootCollection, "Cannot delete a root collection");
}

var collection = await dbContext.Collections.FirstOrDefaultAsync(
c => c.Id == request.CollectionId && c.CustomerId == request.CustomerId,
cancellationToken: cancellationToken);

if (collection is null) return new ResultMessage<DeleteResult, DeleteCollectionType>(DeleteResult.NotFound);

if (collection.Parent is null)
{
return new ResultMessage<DeleteResult, DeleteCollectionType>(DeleteResult.BadRequest,
DeleteCollectionType.CannotDeleteRootCollection, "Cannot delete a root collection");
}

var hasItems = await dbContext.Collections.AnyAsync(
c => c.CustomerId == request.CustomerId && c.Parent == collection.Id,
cancellationToken: cancellationToken);

if (hasItems)
{
return new ResultMessage<DeleteResult, DeleteCollectionType>(DeleteResult.BadRequest,
DeleteCollectionType.CollectionNotEmpty, "Cannot delete a collection with child items");
}

dbContext.Collections.Remove(collection);
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException ex)
{
logger.LogError(ex, "Error attempting to delete collection {CollectionId} for customer {CustomerId}",
request.CollectionId, request.CustomerId);
return new ResultMessage<DeleteResult, DeleteCollectionType>(DeleteResult.Error,
DeleteCollectionType.Unknown, "Error deleting collection");
}

return new ResultMessage<DeleteResult, DeleteCollectionType>(DeleteResult.Deleted);
}
}
21 changes: 16 additions & 5 deletions src/IIIFPresentation/API/Features/Storage/StorageController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task<IActionResult> GetHierarchicalCollection(int customerId, strin
{
var storageRoot = await Mediator.Send(new GetHierarchicalCollection(customerId, slug));

if (storageRoot.Collection is not { IsPublic: true }) return NotFound();
if (storageRoot.Collection is not { IsPublic: true }) return this.PresentationNotFound();

if (Request.ShowExtraProperties())
{
Expand All @@ -51,15 +51,15 @@ public async Task<IActionResult> Get(int customerId, string id, int? page = 1, i
var storageRoot =
await Mediator.Send(new GetCollection(customerId, id, page.Value, pageSize.Value, orderBy, descending));

if (storageRoot.Collection == null) return NotFound();
if (storageRoot.Collection == null) return this.PresentationNotFound();

if (Request.ShowExtraProperties())
{
return Ok(storageRoot.Collection.ToFlatCollection(GetUrlRoots(), pageSize.Value, page.Value,
storageRoot.TotalItems, storageRoot.Items, orderByField));
}

return SeeOther(storageRoot.Collection.GenerateHierarchicalCollectionId(GetUrlRoots())); ;
return SeeOther(storageRoot.Collection.GenerateHierarchicalCollectionId(GetUrlRoots()));
}

[HttpPost("collections")]
Expand All @@ -69,7 +69,7 @@ public async Task<IActionResult> Post(int customerId, [FromBody] UpsertFlatColle
{
if (!Request.ShowExtraProperties())
{
return Problem(statusCode: (int)HttpStatusCode.Forbidden);
return this.PresentationProblem(statusCode: (int)HttpStatusCode.Forbidden);
}

var validation = await validator.ValidateAsync(collection);
Expand All @@ -89,7 +89,7 @@ public async Task<IActionResult> Put(int customerId, string id, [FromBody] Upser
{
if (!Request.ShowExtraProperties())
{
return Problem(statusCode: (int)HttpStatusCode.Forbidden);
return this.PresentationProblem(statusCode: (int)HttpStatusCode.Forbidden);
}

var validation = await validator.ValidateAsync(collection);
Expand All @@ -102,6 +102,17 @@ public async Task<IActionResult> Put(int customerId, string id, [FromBody] Upser
return await HandleUpsert(new UpdateCollection(customerId, id, collection, GetUrlRoots()));
}

[HttpDelete("collections/{id}")]
public async Task<IActionResult> Delete(int customerId, string id)
{
if (!Request.ShowExtraProperties())
{
return this.PresentationProblem(statusCode: (int)HttpStatusCode.Forbidden);
}

return await HandleDelete(new DeleteCollection(customerId, id));
}

private IActionResult SeeOther(string location)
{
Response.Headers.Location = location;
Expand Down
68 changes: 55 additions & 13 deletions src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Core.Helpers;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using Models.API.General;

namespace API.Infrastructure;

Expand All @@ -27,17 +28,17 @@ public static IActionResult FetchResultToHttpResult<T>(this ControllerBase contr
where T : class
{
if (entityResult.Error)
return new ObjectResult(entityResult.ErrorMessage)
{
StatusCode = 500
};
{
return controller.PresentationProblem(detail: entityResult.ErrorMessage,
statusCode: (int)HttpStatusCode.InternalServerError);
}

if (entityResult.EntityNotFound || entityResult.Entity == null) return new NotFoundResult();
if (entityResult.EntityNotFound || entityResult.Entity == null) return controller.PresentationNotFound();

return controller.Ok(entityResult.Entity);
}
/// <summary>

/// <summary>
/// Create an IActionResult from specified ModifyEntityResult{T}.
/// This will be the model + 200/201 on success. Or an
/// error and appropriate status code if failed.
Expand All @@ -63,15 +64,15 @@ public static IActionResult ModifyResultToHttpResult<T>(this ControllerBase cont
WriteResult.Updated => controller.Ok(entityResult.Entity),
WriteResult.Created => controller.Created(controller.Request.GetDisplayUrl(), entityResult.Entity),
WriteResult.NotFound => controller.NotFound(entityResult.Error),
WriteResult.Error => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle),
WriteResult.BadRequest => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.BadRequest, errorTitle),
WriteResult.Conflict => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.Conflict,
WriteResult.Error => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle),
WriteResult.BadRequest => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.BadRequest, errorTitle),
WriteResult.Conflict => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.Conflict,
$"{errorTitle}: Conflict"),
WriteResult.FailedValidation => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.BadRequest,
WriteResult.FailedValidation => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.BadRequest,
$"{errorTitle}: Validation failed"),
WriteResult.StorageLimitExceeded => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.InsufficientStorage,
WriteResult.StorageLimitExceeded => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.InsufficientStorage,
$"{errorTitle}: Storage limit exceeded"),
_ => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle),
_ => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle),
};

/// <summary>
Expand Down Expand Up @@ -105,4 +106,45 @@ public static ObjectResult ValidationFailed(this ControllerBase controller, Vali

return orderByField;
}

/// <summary>
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="Error"/> response.
/// </summary>
/// <param name="statusCode">The value for <see cref="Error.Status" />.</param>
/// <param name="detail">The value for <see cref="Error.Detail" />.</param>
/// <param name="instance">The value for <see cref="Error.Instance" />.</param>
/// <param name="title">The value for <see cref="Error.Title" />.</param>
/// <param name="type">The value for <see cref="Error.Type" />.</param>
/// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
public static ObjectResult PresentationProblem(
this ControllerBase controller,
string? detail = null,
string? instance = null,
int? statusCode = null,
string? title = null,
string? type = null)
{
var error = new Error
{
Detail = detail,
Instance = instance ?? controller.Request.GetDisplayUrl(),
Status = statusCode ?? 500,
Title = title,
ErrorTypeUri = type
};

return new ObjectResult(error)
{
StatusCode = error.Status
};
}

/// <summary>
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="Error"/> response with 404 status code.
/// </summary>
/// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
public static ObjectResult PresentationNotFound(this ControllerBase controller, string? detail = null)
{
return controller.PresentationProblem(detail, null, (int)HttpStatusCode.NotFound, "Not Found");
}
}
Loading

0 comments on commit 12f3c17

Please sign in to comment.