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

Adding delete collection #30

Merged
merged 8 commits into from
Sep 19, 2024
Merged
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()
JackLewis-digirati marked this conversation as resolved.
Show resolved Hide resolved
{
// 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)
JackLewis-digirati marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading