From 3c58a246089cfe7ac8fec17133ac76f5a2369f1a Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Mon, 19 Aug 2024 15:34:38 +0100 Subject: [PATCH 01/24] adding progress with creating collection + testing --- .../Converters/CollectionConverterTests.cs | 3 +- .../Integration/GetCollectionTests.cs | 19 ++--- src/IIIFPresentation/API/API.csproj | 3 + .../API/Converters/CollectionConverter.cs | 7 +- .../Storage/Requests/CreateCollection.cs | 76 +++++++++++++++++-- .../API/Features/Storage/StorageController.cs | 29 +++---- .../Validators/FlatCollectionValidator.cs | 7 +- .../API/Infrastructure/ControllerBaseX.cs | 25 ++++-- .../Infrastructure/PresentationController.cs | 2 +- src/IIIFPresentation/API/Program.cs | 9 ++- src/IIIFPresentation/Core/Core.csproj | 4 + .../Core/Exceptions/PresentationException.cs | 18 +++++ .../Core/Response/HttpResponseMessageX.cs | 49 +++++++++++- .../Collection}/FlatCollection.cs | 31 ++++---- .../Collection}/HierarchicalCollection.cs | 2 +- .../{Response => API/Collection}/Item.cs | 2 +- .../{Response => API/Collection}/PartOf.cs | 2 +- .../Collection}/PresentationType.cs | 2 +- .../{Response => API/Collection}/SeeAlso.cs | 2 +- .../{Response => API/Collection}/View.cs | 2 +- .../Models/API/General/Error.cs | 8 ++ .../Models/Infrastucture/Behavior.cs | 8 ++ 22 files changed, 234 insertions(+), 76 deletions(-) create mode 100644 src/IIIFPresentation/Core/Exceptions/PresentationException.cs rename src/IIIFPresentation/Models/{Response => API/Collection}/FlatCollection.cs (50%) rename src/IIIFPresentation/Models/{Response => API/Collection}/HierarchicalCollection.cs (91%) rename src/IIIFPresentation/Models/{Response => API/Collection}/Item.cs (83%) rename src/IIIFPresentation/Models/{Response => API/Collection}/PartOf.cs (83%) rename src/IIIFPresentation/Models/{Response => API/Collection}/PresentationType.cs (67%) rename src/IIIFPresentation/Models/{Response => API/Collection}/SeeAlso.cs (87%) rename src/IIIFPresentation/Models/{Response => API/Collection}/View.cs (91%) create mode 100644 src/IIIFPresentation/Models/API/General/Error.cs create mode 100644 src/IIIFPresentation/Models/Infrastucture/Behavior.cs diff --git a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs index a17a325d..6c8a080f 100644 --- a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs +++ b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs @@ -1,9 +1,8 @@ using API.Converters; using FluentAssertions; using IIIF.Presentation.V3.Strings; -using Microsoft.EntityFrameworkCore.Query.Internal; +using Models.API.Collection; using Models.Database.Collections; -using Models.Response; namespace API.Tests.Converters; diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index 1df4ae7d..4044a36d 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -4,7 +4,7 @@ using Core.Response; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; -using Models.Response; +using Models.API.Collection; using Repository; using Test.Helpers.Integration; @@ -15,18 +15,11 @@ namespace API.Tests.Integration; public class GetCollectionTests : IClassFixture> { private readonly HttpClient httpClient; - - private readonly PresentationContext dbContext; - + public GetCollectionTests(PresentationContextFixture dbFixture, PresentationAppFactory factory) { - dbContext = dbFixture.DbContext; - httpClient = factory.WithConnectionString(dbFixture.ConnectionString) - .CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false - }); + .CreateClient(new WebApplicationFactoryClientOptions()); } [Fact] @@ -129,7 +122,7 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenAuthAndHeader() response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1/collections/RootStorage"); collection.PublicId.Should().Be("http://localhost/1"); - collection.Items.Count.Should().Be(1); + collection.Items!.Count.Should().Be(1); collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); collection.TotalItems.Should().Be(1); collection.CreatedBy.Should().Be("admin"); @@ -153,7 +146,7 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenCalledById() response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1/collections/RootStorage"); collection.PublicId.Should().Be("http://localhost/1"); - collection.Items.Count.Should().Be(1); + collection.Items!.Count.Should().Be(1); collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); collection.TotalItems.Should().Be(1); collection.CreatedBy.Should().Be("admin"); @@ -177,7 +170,7 @@ public async Task Get_ChildFlat_ReturnsEntryPointFlat_WhenCalledByChildId() response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); collection.PublicId.Should().Be("http://localhost/1/first-child"); - collection.Items.Count.Should().Be(1); + collection.Items!.Count.Should().Be(1); collection.Items[0].Id.Should().Be("http://localhost/1/collections/SecondChildCollection"); collection.TotalItems.Should().Be(1); collection.CreatedBy.Should().Be("admin"); diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index bf81adf9..b48105de 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -27,6 +27,9 @@ .dockerignore + + Always + diff --git a/src/IIIFPresentation/API/Converters/CollectionConverter.cs b/src/IIIFPresentation/API/Converters/CollectionConverter.cs index 5398af47..c654a148 100644 --- a/src/IIIFPresentation/API/Converters/CollectionConverter.cs +++ b/src/IIIFPresentation/API/Converters/CollectionConverter.cs @@ -1,6 +1,7 @@ using API.Infrastructure.Helpers; +using Models.API.Collection; using Models.Database.Collections; -using Models.Response; +using Models.Infrastucture; namespace API.Converters; @@ -40,8 +41,8 @@ public static FlatCollection ToFlatCollection(this Collection dbAsset, UrlRoots Label = dbAsset.Label, PublicId = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}{(dbAsset.FullPath != null ? $"/{dbAsset.FullPath}" : "")}", Behavior = new List() - .AppendIf(dbAsset.IsPublic, "public-iiif") - .AppendIf(dbAsset.IsStorageCollection, "storage-collection"), + .AppendIf(dbAsset.IsPublic, Behavior.IsPublic) + .AppendIf(dbAsset.IsStorageCollection, Behavior.IsStorageCollection), Type = PresentationType.Collection, Slug = dbAsset.Slug, Parent = dbAsset.Parent != null ? $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}/collections/{dbAsset.Parent}" : null, diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index 214c1487..c361a562 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -1,26 +1,92 @@ -using API.Infrastructure.Requests; +using API.Converters; +using API.Infrastructure.Requests; +using API.Settings; +using Core; using MediatR; -using Models.Response; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Models.API.Collection; +using Models.Database.Collections; +using Models.Infrastucture; +using Repository; namespace API.Features.Storage.Requests; public class CreateCollection : IRequest> { - public CreateCollection(int customerId, FlatCollection collection) + public CreateCollection(int customerId, FlatCollection collection, UrlRoots urlRoots) { CustomerId = customerId; Collection = collection; + UrlRoots = urlRoots; } public int CustomerId { get; } public FlatCollection Collection { get; } + + public UrlRoots UrlRoots { get; } } public class CreateCollectionHandler : IRequestHandler> { - public Task> Handle(CreateCollection request, CancellationToken cancellationToken) + private readonly PresentationContext dbContext; + + private readonly ILogger logger; + + private readonly ApiSettings settings; + + public CreateCollectionHandler( + PresentationContext dbContext, + ILogger logger, + IOptions options) + { + this.dbContext = dbContext; + this.logger = logger; + settings = options.Value; + } + + public async Task> Handle(CreateCollection request, CancellationToken cancellationToken) + { + var collection = new Collection() + { + Id = Guid.NewGuid().ToString(), + Created = DateTime.UtcNow, + Modified = DateTime.UtcNow, + CreatedBy = GetUser(), //TODO: update this to get a real user + CustomerId = request.CustomerId, + IsPublic = request.Collection.Behavior.Contains(Behavior.IsPublic), + IsStorageCollection = request.Collection.Behavior.Contains(Behavior.IsStorageCollection), + ItemsOrder = request.Collection.ItemsOrder, + Label = request.Collection.Label, + Parent = request.Collection.Parent!.Split('/').Last(), + Slug = request.Collection.Slug, + Thumbnail = request.Collection.Thumbnail, + Tags = request.Collection.Tags + }; + + dbContext.Collections.Add(collection); + + try + { + await dbContext.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) + { + logger.LogError(ex,"Error creating collection"); + + return ModifyEntityResult.Failure( + $"The collection could not be created"); + } + + return ModifyEntityResult.Success( + collection.ToFlatCollection(request.UrlRoots, settings.PageSize, + new EnumerableQuery(new List())), // there can be no items attached to this as it's just been created + WriteResult.Created); + } + + private string? GetUser() { - throw new NotImplementedException(); + return "Admin"; } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index d0b98bde..5cbf9b0a 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -3,13 +3,14 @@ using API.Auth; using API.Converters; using API.Features.Storage.Requests; +using API.Features.Storage.Validators; using API.Infrastructure; -using API.Infrastructure.Requests; using API.Settings; using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using Models.Response; +using FluentValidation; +using Models.API.Collection; namespace API.Features.Storage; @@ -63,28 +64,22 @@ public async Task Get(int customerId, string id) [HttpPost("collections")] [EtagCaching] - public async Task Post(int customerId, [FromBody] FlatCollection collection) + public async Task Post(int customerId, [FromBody] FlatCollection collection, [FromServices] FlatCollectionValidator validator) { if (!Authorizer.CheckAuthorized(Request)) { return Problem(statusCode: (int)HttpStatusCode.Forbidden); } - - var created = await Mediator.Send(new CreateCollection(customerId, collection)); - return Ok(created); - } + var validation = await validator.ValidateAsync(collection, policy => policy.IncludeRuleSets("create")); - /// - /// Used by derived controllers to construct correct fully qualified URLs in returned objects. - /// - /// - protected UrlRoots GetUrlRoots() - { - return new UrlRoots + if (!validation.IsValid) { - BaseUrl = Request.GetBaseUrl(), - ResourceRoot = Settings.ResourceRoot.ToString() - }; + return this.ValidationFailed(validation); + } + + var created = await Mediator.Send(new CreateCollection(customerId, collection, GetUrlRoots())); + + return Ok(created); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs b/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs index 6a7af295..b9f58f04 100644 --- a/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs +++ b/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs @@ -1,5 +1,5 @@ using FluentValidation; -using Models.Response; +using Models.API.Collection; namespace API.Features.Storage.Validators; @@ -11,9 +11,8 @@ public FlatCollectionValidator() { RuleFor(a => a.Created).Empty().WithMessage("Created cannot be set"); RuleFor(a => a.Modified).Empty().WithMessage("Modified cannot be set"); + RuleFor(a => a.Id).Empty().WithMessage("Id cannot be set"); + RuleFor(a => a.Parent).NotEmpty().WithMessage("Creating a new collection requires a parent"); }); - - - } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs b/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs index 60d5207d..6a774261 100644 --- a/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs @@ -1,5 +1,6 @@ -using API.Infrastructure.Requests; +using API.Infrastructure.Requests; using Core; +using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; namespace API.Infrastructure; @@ -58,14 +59,24 @@ public static IActionResult ModifyResultToHttpResult(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, 500, errorTitle), - WriteResult.BadRequest => controller.Problem(entityResult.Error, instance, 400, errorTitle), - WriteResult.Conflict => controller.Problem(entityResult.Error, instance, 409, + 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, $"{errorTitle}: Conflict"), - WriteResult.FailedValidation => controller.Problem(entityResult.Error, instance, 400, + WriteResult.FailedValidation => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.BadRequest, $"{errorTitle}: Validation failed"), - WriteResult.StorageLimitExceeded => controller.Problem(entityResult.Error, instance, 507, + WriteResult.StorageLimitExceeded => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.InsufficientStorage, $"{errorTitle}: Storage limit exceeded"), - _ => controller.Problem(entityResult.Error, instance, 500, errorTitle), + _ => controller.Problem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle), }; + + /// + /// Creates an that produces a response with 404 status code. + /// + /// The created for the response. + public static ObjectResult ValidationFailed(this ControllerBase controller, ValidationResult validationResult) + { + var message = string.Join(". ", validationResult.Errors.Select(s => s.ErrorMessage).Distinct()); + return controller.Problem(message, null, (int)HttpStatusCode.BadRequest, "Bad request"); + } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index 770848f2..cc43becc 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -20,7 +20,7 @@ public abstract class PresentationController : Controller /// protected PresentationController(ApiSettings settings, IMediator mediator) { - Settings = settings; + this.Settings = settings; Mediator = mediator; } diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 29001e23..49af00d1 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using API.Features.Storage.Validators; using API.Infrastructure; using API.Settings; using Repository; @@ -11,9 +12,9 @@ .WriteTo.Console() .CreateLogger(); -builder.Services.AddSerilog(lc => lc - .WriteTo.Console() - .ReadFrom.Configuration(builder.Configuration)); +// builder.Services.AddSerilog(lc => lc +// .WriteTo.Console() +// .ReadFrom.Configuration(builder.Configuration)); builder.Services.AddControllers().AddJsonOptions(opt => { @@ -21,6 +22,8 @@ opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); +builder.Services.AddScoped(); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/src/IIIFPresentation/Core/Core.csproj b/src/IIIFPresentation/Core/Core.csproj index a9c778a7..38f44843 100644 --- a/src/IIIFPresentation/Core/Core.csproj +++ b/src/IIIFPresentation/Core/Core.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/src/IIIFPresentation/Core/Exceptions/PresentationException.cs b/src/IIIFPresentation/Core/Exceptions/PresentationException.cs new file mode 100644 index 00000000..debf9e84 --- /dev/null +++ b/src/IIIFPresentation/Core/Exceptions/PresentationException.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; + +namespace Core.Exceptions; + +public class PresentationException : Exception +{ + public PresentationException() + { + } + + public PresentationException(string? message) : base(message) + { + } + + public PresentationException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs index 0b3f050a..11af0e25 100644 --- a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs +++ b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +using Core.Exceptions; +using Models.API.General; +using Newtonsoft.Json; namespace Core.Response; @@ -32,7 +34,15 @@ public static class HttpResponseMessageX serializer.ContractResolver = settings.ContractResolver; } serializer.NullValueHandling = settings.NullValueHandling; - return serializer.Deserialize(jsonReader); + + try + { + return serializer.Deserialize(jsonReader); + } + catch (Exception exception) + { + return serializer.Deserialize(jsonReader); + } } /// @@ -46,4 +56,39 @@ public static bool IsJsonResponse(this HttpResponseMessage response) var mediaType = response.Content.Headers.ContentType?.MediaType; return mediaType != null && mediaType.Contains("json"); } + + public static async Task ReadAsPresentationResponseAsync(this HttpResponseMessage response, + JsonSerializerSettings? settings = null) + { + if ((int)response.StatusCode < 400) + { + return await response.ReadWithContext(true, settings); + } + + Error? error; + try + { + error = await response.ReadAsJsonAsync(false, settings); + } + catch (Exception ex) + { + throw new PresentationException("Could not find a Hydra error in response", ex); + } + + if (error != null) + { + throw new PresentationException(error.Description); + } + + throw new PresentationException("Unable to process error condition"); + } + + private static async Task ReadWithContext( + this HttpResponseMessage response, + bool ensureSuccess, + JsonSerializerSettings? settings) + { + var json = await response.ReadAsJsonAsync(ensureSuccess, settings); + return json; + } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Response/FlatCollection.cs b/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs similarity index 50% rename from src/IIIFPresentation/Models/Response/FlatCollection.cs rename to src/IIIFPresentation/Models/API/Collection/FlatCollection.cs index acb89af6..9cbe8433 100644 --- a/src/IIIFPresentation/Models/Response/FlatCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs @@ -1,37 +1,38 @@ using System.Text.Json.Serialization; +using IIIF.Presentation.V3.Strings; -namespace Models.Response; +namespace Models.API.Collection; public class FlatCollection { [JsonPropertyName("@context")] - public List Context { get; set; } + public List? Context { get; set; } - public string Id { get; set; } + public string? Id { get; set; } - public string PublicId { get; set; } + public string? PublicId { get; set; } public PresentationType Type { get; set; } - - public List Behavior { get; set; } - - public Dictionary> Label { get; set; } - - public string Slug { get; set; } + + public List Behavior { get; set; } = new (); + + public LanguageMap Label { get; set; } = null!; + + public string Slug { get; set; } = null!; public string? Parent { get; set; } public int? ItemsOrder { get; set; } - public List Items { get; set; } + public List? Items { get; set; } public List? PartOf { get; set; } public int TotalItems { get; set; } - public View View { get; set; } = new View(); + public View? View { get; set; } - public List SeeAlso { get; set; } + public List? SeeAlso { get; set; } public DateTime Created { get; set; } @@ -40,4 +41,8 @@ public class FlatCollection public string? CreatedBy { get; set; } public string? ModifiedBy { get; set; } + + public string? Tags { get; set; } + + public string? Thumbnail { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Response/HierarchicalCollection.cs b/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs similarity index 91% rename from src/IIIFPresentation/Models/Response/HierarchicalCollection.cs rename to src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs index 38c22dcf..b1197c96 100644 --- a/src/IIIFPresentation/Models/Response/HierarchicalCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Models.Response; +namespace Models.API.Collection; public class HierarchicalCollection { diff --git a/src/IIIFPresentation/Models/Response/Item.cs b/src/IIIFPresentation/Models/API/Collection/Item.cs similarity index 83% rename from src/IIIFPresentation/Models/Response/Item.cs rename to src/IIIFPresentation/Models/API/Collection/Item.cs index 889ed8ed..f5a11df9 100644 --- a/src/IIIFPresentation/Models/Response/Item.cs +++ b/src/IIIFPresentation/Models/API/Collection/Item.cs @@ -1,4 +1,4 @@ -namespace Models.Response; +namespace Models.API.Collection; public class Item { diff --git a/src/IIIFPresentation/Models/Response/PartOf.cs b/src/IIIFPresentation/Models/API/Collection/PartOf.cs similarity index 83% rename from src/IIIFPresentation/Models/Response/PartOf.cs rename to src/IIIFPresentation/Models/API/Collection/PartOf.cs index b187ed56..c3c94710 100644 --- a/src/IIIFPresentation/Models/Response/PartOf.cs +++ b/src/IIIFPresentation/Models/API/Collection/PartOf.cs @@ -1,4 +1,4 @@ -namespace Models.Response; +namespace Models.API.Collection; public class PartOf { diff --git a/src/IIIFPresentation/Models/Response/PresentationType.cs b/src/IIIFPresentation/Models/API/Collection/PresentationType.cs similarity index 67% rename from src/IIIFPresentation/Models/Response/PresentationType.cs rename to src/IIIFPresentation/Models/API/Collection/PresentationType.cs index a26ffd95..ca7a869e 100644 --- a/src/IIIFPresentation/Models/Response/PresentationType.cs +++ b/src/IIIFPresentation/Models/API/Collection/PresentationType.cs @@ -1,4 +1,4 @@ -namespace Models.Response; +namespace Models.API.Collection; public enum PresentationType { diff --git a/src/IIIFPresentation/Models/Response/SeeAlso.cs b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs similarity index 87% rename from src/IIIFPresentation/Models/Response/SeeAlso.cs rename to src/IIIFPresentation/Models/API/Collection/SeeAlso.cs index 71aa3b15..56ed93ae 100644 --- a/src/IIIFPresentation/Models/Response/SeeAlso.cs +++ b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs @@ -1,4 +1,4 @@ -namespace Models.Response; +namespace Models.API.Collection; public class SeeAlso { diff --git a/src/IIIFPresentation/Models/Response/View.cs b/src/IIIFPresentation/Models/API/Collection/View.cs similarity index 91% rename from src/IIIFPresentation/Models/Response/View.cs rename to src/IIIFPresentation/Models/API/Collection/View.cs index a2f49497..405812e9 100644 --- a/src/IIIFPresentation/Models/Response/View.cs +++ b/src/IIIFPresentation/Models/API/Collection/View.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Models.Response; +namespace Models.API.Collection; public class View { diff --git a/src/IIIFPresentation/Models/API/General/Error.cs b/src/IIIFPresentation/Models/API/General/Error.cs new file mode 100644 index 00000000..65f1f016 --- /dev/null +++ b/src/IIIFPresentation/Models/API/General/Error.cs @@ -0,0 +1,8 @@ +namespace Models.API.General; + +public class Error +{ + public string Type => "Error"; + + public string? Description { get; set; } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Infrastucture/Behavior.cs b/src/IIIFPresentation/Models/Infrastucture/Behavior.cs new file mode 100644 index 00000000..2f8f1c6f --- /dev/null +++ b/src/IIIFPresentation/Models/Infrastucture/Behavior.cs @@ -0,0 +1,8 @@ +namespace Models.Infrastucture; + +public static class Behavior +{ + public static string IsPublic = "public-iiif"; + + public static string IsStorageCollection = "storage-collection"; +} \ No newline at end of file From 99de0d0cde2a500c3304399d670d4f87c1e0848e Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Mon, 19 Aug 2024 15:56:08 +0100 Subject: [PATCH 02/24] adding current create integration tests --- .../Integration/CreateCollectionTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs diff --git a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs new file mode 100644 index 00000000..a423662c --- /dev/null +++ b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs @@ -0,0 +1,81 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using API.Infrastructure.Requests; +using API.Tests.Integration.Infrastucture; +using Core.Response; +using FluentAssertions; +using IIIF.Presentation.V3.Strings; +using Microsoft.AspNetCore.Mvc.Testing; +using Models.API.Collection; +using Models.Infrastucture; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Repository; +using Test.Helpers.Integration; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace API.Tests.Integration; + +[Trait("Category", "Integration")] +[Collection(CollectionDefinitions.DatabaseCollection.CollectionName)] +public class CreateCollectionTests : IClassFixture> +{ + private readonly HttpClient httpClient; + + private readonly PresentationContext dbContext; + + private const int Customer = 1; + + private readonly string parent; + + public CreateCollectionTests(PresentationContextFixture dbFixture, PresentationAppFactory factory) + { + dbContext = dbFixture.DbContext; + + httpClient = factory.WithConnectionString(dbFixture.ConnectionString) + .CreateClient(new WebApplicationFactoryClientOptions()); + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer"); + + parent = dbContext.Collections.FirstOrDefault(x => x.CustomerId == Customer && x.Slug == string.Empty)! + .Id!; + } + + [Fact] + public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided() + { + // Arrange + var collection = new FlatCollection() + { + Behavior = new List() + { + Behavior.IsPublic, + Behavior.IsStorageCollection + }, + Label = new LanguageMap("en", new []{"test collection"}), + Slug = "programmatic-child", + Parent = parent + }; + + // Act + var response = await httpClient.PostAsync($"{Customer}/collections", + new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, + new MediaTypeHeaderValue("application/json"))); + var test = await response.Content.ReadAsStringAsync(); + + var stuff = JsonSerializer.Deserialize>(test, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + var responseCollection = await response.ReadAsPresentationResponseAsync>(new JsonSerializerSettings() + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + var fromDatabase = dbContext.Collections.FirstOrDefault(c => c.Id == responseCollection!.Entity!.Id); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + } +} \ No newline at end of file From 09ae61b9c3b209a9654aea89a6715eed007a9877 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Tue, 20 Aug 2024 11:10:37 +0100 Subject: [PATCH 03/24] adding missing using --- src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs b/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs index 6a774261..854ffa55 100644 --- a/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Runtime.InteropServices.JavaScript; using API.Infrastructure.Requests; using Core; using FluentValidation.Results; From ced0422feb9029914a16b770318dda96303bfe82 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Tue, 20 Aug 2024 15:56:29 +0100 Subject: [PATCH 04/24] updates to add index + tests --- .../Integration/CreateCollectionTests.cs | 47 ++++++-- .../API.Tests/appsettings.Testing.json | 3 +- .../Storage/Requests/CreateCollection.cs | 14 ++- .../API/Features/Storage/StorageController.cs | 4 +- src/IIIFPresentation/API/Program.cs | 5 +- .../Core/Response/HttpResponseMessageX.cs | 2 +- .../Models/API/General/Error.cs | 4 +- ...142445_addingCustomerSlugIndex.Designer.cs | 108 ++++++++++++++++++ .../20240820142445_addingCustomerSlugIndex.cs | 28 +++++ .../PresentationContextModelSnapshot.cs | 4 + .../Repository/PresentationContext.cs | 2 + 11 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.cs diff --git a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs index a423662c..8d72eb8a 100644 --- a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs @@ -2,18 +2,22 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using API.Infrastructure.Requests; using API.Tests.Integration.Infrastucture; +using Core.Exceptions; using Core.Response; using FluentAssertions; using IIIF.Presentation.V3.Strings; using Microsoft.AspNetCore.Mvc.Testing; using Models.API.Collection; +using Models.API.General; using Models.Infrastucture; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Repository; using Test.Helpers.Integration; +using JsonConverter = Newtonsoft.Json.JsonConverter; using JsonSerializer = System.Text.Json.JsonSerializer; namespace API.Tests.Integration; @@ -63,19 +67,46 @@ public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided() var response = await httpClient.PostAsync($"{Customer}/collections", new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, new MediaTypeHeaderValue("application/json"))); - var test = await response.Content.ReadAsStringAsync(); - var stuff = JsonSerializer.Deserialize>(test, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var responseCollection = await response.ReadAsPresentationResponseAsync(); - var responseCollection = await response.ReadAsPresentationResponseAsync>(new JsonSerializerSettings() + var fromDatabase = dbContext.Collections.First(c => c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last()); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + fromDatabase.Parent.Should().Be(parent); + fromDatabase.Label.Values.First()[0].Should().Be("test collection"); + fromDatabase.Slug.Should().Be("programmatic-child"); + fromDatabase.IsPublic.Should().BeTrue(); + fromDatabase.IsStorageCollection.Should().BeTrue(); + } + + [Fact] + public async Task CreateCollection_FailsToCreateCollection_WhenDuplicateSlug() + { + // Arrange + var collection = new FlatCollection() { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); + Behavior = new List() + { + Behavior.IsPublic, + Behavior.IsStorageCollection + }, + Label = new LanguageMap("en", new []{"test collection"}), + Slug = "first-child", + Parent = parent + }; - var fromDatabase = dbContext.Collections.FirstOrDefault(c => c.Id == responseCollection!.Entity!.Id); + // Act + var response = await httpClient.PostAsync($"{Customer}/collections", + new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, + new MediaTypeHeaderValue("application/json"))); - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); + Func action = async () => await response.ReadAsPresentationResponseAsync(); + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + await action.Should().ThrowAsync() + .WithMessage("The collection could not be created due to a duplicate slug value"); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API.Tests/appsettings.Testing.json b/src/IIIFPresentation/API.Tests/appsettings.Testing.json index bf46c9a9..9b126fac 100644 --- a/src/IIIFPresentation/API.Tests/appsettings.Testing.json +++ b/src/IIIFPresentation/API.Tests/appsettings.Testing.json @@ -18,6 +18,7 @@ }, "RunMigrations": true, "ApiSettings": { - "ResourceRoot": "https://localhost:7230" + "ResourceRoot": "https://localhost:7230", + "PageSize": 20 } } diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index c361a562..644fb9a8 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -74,9 +74,17 @@ public async Task> Handle(CreateCollection re catch (DbUpdateException ex) { logger.LogError(ex,"Error creating collection"); - - return ModifyEntityResult.Failure( - $"The collection could not be created"); + + if (ex.InnerException != null && ex.InnerException.Message.Contains("uplicate key value violates unique constraint \"ix_collections_customer_id_slug\"")) + { + return ModifyEntityResult.Failure( + $"The collection could not be created due to a duplicate slug value", WriteResult.BadRequest); + } + else + { + return ModifyEntityResult.Failure( + $"The collection could not be created"); + } } return ModifyEntityResult.Success( diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index 5cbf9b0a..4a9cef80 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -78,8 +78,6 @@ public async Task Post(int customerId, [FromBody] FlatCollection return this.ValidationFailed(validation); } - var created = await Mediator.Send(new CreateCollection(customerId, collection, GetUrlRoots())); - - return Ok(created); + return await HandleUpsert(new CreateCollection(customerId, collection, GetUrlRoots())); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 49af00d1..5b5b2c38 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -12,9 +12,8 @@ .WriteTo.Console() .CreateLogger(); -// builder.Services.AddSerilog(lc => lc -// .WriteTo.Console() -// .ReadFrom.Configuration(builder.Configuration)); +builder.Services.AddSerilog(lc => lc + .ReadFrom.Configuration(builder.Configuration)); builder.Services.AddControllers().AddJsonOptions(opt => { diff --git a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs index 11af0e25..8c5612ed 100644 --- a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs +++ b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs @@ -77,7 +77,7 @@ public static bool IsJsonResponse(this HttpResponseMessage response) if (error != null) { - throw new PresentationException(error.Description); + throw new PresentationException(error.Detail); } throw new PresentationException("Unable to process error condition"); diff --git a/src/IIIFPresentation/Models/API/General/Error.cs b/src/IIIFPresentation/Models/API/General/Error.cs index 65f1f016..5d90d165 100644 --- a/src/IIIFPresentation/Models/API/General/Error.cs +++ b/src/IIIFPresentation/Models/API/General/Error.cs @@ -4,5 +4,7 @@ public class Error { public string Type => "Error"; - public string? Description { get; set; } + public string? Detail { get; set; } + + public int Status { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.Designer.cs new file mode 100644 index 00000000..f2b5c809 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.Designer.cs @@ -0,0 +1,108 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Repository; + +#nullable disable + +namespace Repository.Migrations +{ + [DbContext(typeof(PresentationContext))] + [Migration("20240820142445_addingCustomerSlugIndex")] + partial class addingCustomerSlugIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("IsStorageCollection") + .HasColumnType("boolean") + .HasColumnName("is_storage_collection"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Label") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("LockedBy") + .HasColumnType("text") + .HasColumnName("locked_by"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.Property("UsePath") + .HasColumnType("boolean") + .HasColumnName("use_path"); + + b.HasKey("Id") + .HasName("pk_collections"); + + b.HasIndex("CustomerId", "Slug") + .IsUnique() + .HasDatabaseName("ix_collections_customer_id_slug"); + + b.ToTable("collections", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.cs b/src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.cs new file mode 100644 index 00000000..4ce08d5f --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20240820142445_addingCustomerSlugIndex.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class addingCustomerSlugIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "ix_collections_customer_id_slug", + table: "collections", + columns: new[] { "customer_id", "slug" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_collections_customer_id_slug", + table: "collections"); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index 01171a79..0785a388 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -93,6 +93,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_collections"); + b.HasIndex("CustomerId", "Slug") + .IsUnique() + .HasDatabaseName("ix_collections_customer_id_slug"); + b.ToTable("collections", (string)null); }); #pragma warning restore 612, 618 diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 874d8ffe..b6e0abf8 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -29,6 +29,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { + entity.HasIndex(e => new { e.CustomerId, e.Slug }).IsUnique(); + entity.Property(e => e.Label).HasColumnType("jsonb"); }); } From a74bcdf2b97dad413c21abfd0a4c58ddc87ff71e Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 11:47:36 +0100 Subject: [PATCH 05/24] changes to allow builds --- .../actions/docker-build-and-push/action.yml | 53 +++++++++ .github/workflows/run_build.yml | 65 +++++++++++ src/IIIFPresentation/.dockerignore | 25 ++++ src/IIIFPresentation/AWS/AWS.csproj | 18 +++ .../AWS/SSM/ConfigurationBuilderX.cs | 24 ++++ src/IIIFPresentation/Dockerfile.API | 29 +++++ src/IIIFPresentation/Dockerfile.Migrator | 28 +++++ src/IIIFPresentation/IIIFPresentation.sln | 15 +++ src/IIIFPresentation/Migrator/Dockerfile | 18 +++ src/IIIFPresentation/Migrator/Migrator.csproj | 29 +++++ src/IIIFPresentation/Migrator/Program.cs | 64 +++++++++++ ...0240821102322_addParentToIndex.Designer.cs | 108 ++++++++++++++++++ .../20240821102322_addParentToIndex.cs | 38 ++++++ .../PresentationContextModelSnapshot.cs | 4 +- .../Repository/PresentationContext.cs | 2 +- 15 files changed, 517 insertions(+), 3 deletions(-) create mode 100644 .github/actions/docker-build-and-push/action.yml create mode 100644 .github/workflows/run_build.yml create mode 100644 src/IIIFPresentation/.dockerignore create mode 100644 src/IIIFPresentation/AWS/AWS.csproj create mode 100644 src/IIIFPresentation/AWS/SSM/ConfigurationBuilderX.cs create mode 100644 src/IIIFPresentation/Dockerfile.API create mode 100644 src/IIIFPresentation/Dockerfile.Migrator create mode 100644 src/IIIFPresentation/Migrator/Dockerfile create mode 100644 src/IIIFPresentation/Migrator/Migrator.csproj create mode 100644 src/IIIFPresentation/Migrator/Program.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.cs diff --git a/.github/actions/docker-build-and-push/action.yml b/.github/actions/docker-build-and-push/action.yml new file mode 100644 index 00000000..3bb92af6 --- /dev/null +++ b/.github/actions/docker-build-and-push/action.yml @@ -0,0 +1,53 @@ +name: Docker Build & Push +description: Composite GitHub Action to build and push Docker images to the DLCS GitHub Packages repositories. + +inputs: + image-name: + description: "Name of the image to push to the GHCR repository." + required: true + dockerfile: + description: "The Dockerfile to build and push." + required: true + context: + description: "The context to use when building the Dockerfile." + required: true + github-token: + description: "The GitHub token used when interacting with GCHR." + required: true + +runs: + using: "composite" + steps: + - id: checkout + uses: actions/checkout@v4 + - id: docker-setup-buildx + uses: docker/setup-buildx-action@v2 + with: + driver-opts: | + image=moby/buildkit:v0.10.6 + - id: docker-meta + uses: docker/metadata-action@v3 + with: + images: ghcr.io/dlcs/${{ inputs.image-name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,enable=true,prefix=,format=long + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - id: docker-login + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.github-token }} + - id: docker-build-push + uses: docker/build-push-action@v2 + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + builder: ${{ steps.docker-setup-buildx.outputs.name }} + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + push: ${{ github.actor != 'dependabot[bot]' }} diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml new file mode 100644 index 00000000..0563ed92 --- /dev/null +++ b/.github/workflows/run_build.yml @@ -0,0 +1,65 @@ +name: DLCS Build, Test & Publish + +on: + push: + branches: [ "main", "develop" ] + tags: [ "v*" ] + pull_request: + branches: [ "main", "develop" ] + paths-ignore: + - "docs/**" + - "scripts/**" + +jobs: + test-dotnet: + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/IIIFPresentation + env: + BUILD_CONFIG: "Release" + SOLUTION: "IIIFPresentation.sln" + steps: + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'zulu' # Alternative distribution options are available. + - id: checkout + uses: actions/checkout@v4 + - id: setup-dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + - id: restore-dotnet-dependencies + run: dotnet restore $SOLUTION + - id: build-dotnet + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + run: | + dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore + dotnet dotcover test --dcReportType=HTML --filter 'Category!=Manual' --configuration $BUILD_CONFIG --no-restore --no-build --verbosity normal + + build-push-api: + runs-on: ubuntu-latest + needs: test-dotnet + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/docker-build-and-push + with: + image-name: "api" + dockerfile: "Dockerfile.API" + context: "./src/IIIFPresentation" + github-token: ${{ secrets.GITHUB_TOKEN }} + + build-push-migrator: + runs-on: ubuntu-latest + needs: test-dotnet + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/docker-build-and-push + with: + image-name: "migrator" + dockerfile: "Dockerfile.Migrator" + context: "./src/IIIFPresentation" + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/src/IIIFPresentation/.dockerignore b/src/IIIFPresentation/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/src/IIIFPresentation/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/IIIFPresentation/AWS/AWS.csproj b/src/IIIFPresentation/AWS/AWS.csproj new file mode 100644 index 00000000..308f512d --- /dev/null +++ b/src/IIIFPresentation/AWS/AWS.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/src/IIIFPresentation/AWS/SSM/ConfigurationBuilderX.cs b/src/IIIFPresentation/AWS/SSM/ConfigurationBuilderX.cs new file mode 100644 index 00000000..82f6b6f9 --- /dev/null +++ b/src/IIIFPresentation/AWS/SSM/ConfigurationBuilderX.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace AWS.SSM; + +public static class ConfigurationBuilderX +{ + /// + /// Add AWS SystemsManager (SSM) as a configuration source if Production hosting environment. + /// By default prefix is /iiif-presentation/ but this can be overriden via SSM_PREFIX envvar + /// + public static IConfigurationBuilder AddSystemsManager(this IConfigurationBuilder builder, + HostBuilderContext builderContext) + { + if (!builderContext.HostingEnvironment.IsProduction()) return builder; + + var path = Environment.GetEnvironmentVariable("SSM_PREFIX") ?? "iiif-presentation"; + return builder.AddSystemsManager(configureSource => + { + configureSource.Path = $"/{path}/"; + configureSource.ReloadAfter = TimeSpan.FromMinutes(90); + }); + } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Dockerfile.API b/src/IIIFPresentation/Dockerfile.API new file mode 100644 index 00000000..b67fe1c9 --- /dev/null +++ b/src/IIIFPresentation/Dockerfile.API @@ -0,0 +1,29 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src + +COPY ["API/API.csproj", "API/"] +COPY ["Repository/Repository.csproj", "DLCS.Repository/"] +COPY ["Models/Models.csproj", "DLCS.Model/"] +COPY ["Core/Core.csproj", "DLCS.Core/"] +COPY ["Web/Web.csproj", "Web/"] +#COPY ["AWS/AWS.csproj", "DLCS.AWS/"] + +RUN dotnet restore "API/API.csproj" + +COPY . . +WORKDIR "/src/API" +RUN dotnet build "API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "API.csproj" -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base + +LABEL maintainer="Donald Gray ,Tom Crane " +LABEL org.opencontainers.image.source=https://github.com/dlcs/protagonist +LABEL org.opencontainers.image.description="HTTP API for DLCS interactions." + +WORKDIR /app +EXPOSE 80 +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "API.dll"] \ No newline at end of file diff --git a/src/IIIFPresentation/Dockerfile.Migrator b/src/IIIFPresentation/Dockerfile.Migrator new file mode 100644 index 00000000..75835251 --- /dev/null +++ b/src/IIIFPresentation/Dockerfile.Migrator @@ -0,0 +1,28 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src + +COPY ["Utils/Migrator/Migrator.csproj", "Utils/Migrator/"] +COPY ["Repository/Repository.csproj", "DLCS.Repository/"] +COPY ["DLCS.AWS/DLCS.AWS.csproj", "DLCS.AWS/"] +COPY ["Models/Models.csproj", "DLCS.Model/"] +COPY ["Core/Core.csproj", "DLCS.Core/"] + +RUN dotnet restore "Utils/Migrator/Migrator.csproj" + +COPY . . +WORKDIR "/src/Utils/Migrator" +RUN dotnet build "Migrator.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base + +LABEL maintainer="Donald Gray " +LABEL org.opencontainers.image.source=https://github.com/dlcs/protagonist +LABEL org.opencontainers.image.description="EF Migration runner for DLCS DB" + +WORKDIR /app +EXPOSE 80 +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Migrator.dll"] \ No newline at end of file diff --git a/src/IIIFPresentation/IIIFPresentation.sln b/src/IIIFPresentation/IIIFPresentation.sln index 694a473e..e7a96f7a 100644 --- a/src/IIIFPresentation/IIIFPresentation.sln +++ b/src/IIIFPresentation/IIIFPresentation.sln @@ -14,6 +14,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "API.Tests\API. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Helpers", "Test.Helpers\Test.Helpers.csproj", "{C8CDD6D9-EC7D-4366-9C41-5B1C93777AC8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{CFEA077C-542A-44A2-A4BD-1725A2FD40D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Migrator", "Migrator\Migrator.csproj", "{B6491452-9798-42B9-8573-1ED2454DE1BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS", "AWS\AWS.csproj", "{3F9BB7E8-E0F3-4891-8765-28EB50669C58}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,9 +50,18 @@ Global {C8CDD6D9-EC7D-4366-9C41-5B1C93777AC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8CDD6D9-EC7D-4366-9C41-5B1C93777AC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8CDD6D9-EC7D-4366-9C41-5B1C93777AC8}.Release|Any CPU.Build.0 = Release|Any CPU + {B6491452-9798-42B9-8573-1ED2454DE1BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6491452-9798-42B9-8573-1ED2454DE1BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6491452-9798-42B9-8573-1ED2454DE1BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6491452-9798-42B9-8573-1ED2454DE1BD}.Release|Any CPU.Build.0 = Release|Any CPU + {3F9BB7E8-E0F3-4891-8765-28EB50669C58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F9BB7E8-E0F3-4891-8765-28EB50669C58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F9BB7E8-E0F3-4891-8765-28EB50669C58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F9BB7E8-E0F3-4891-8765-28EB50669C58}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CDB60D14-DA74-486F-AE46-F4D6F801DE21} = {23CF47D6-E80B-4B6A-828D-4635800CD326} {C8CDD6D9-EC7D-4366-9C41-5B1C93777AC8} = {23CF47D6-E80B-4B6A-828D-4635800CD326} + {B6491452-9798-42B9-8573-1ED2454DE1BD} = {CFEA077C-542A-44A2-A4BD-1725A2FD40D5} EndGlobalSection EndGlobal diff --git a/src/IIIFPresentation/Migrator/Dockerfile b/src/IIIFPresentation/Migrator/Dockerfile new file mode 100644 index 00000000..412dd4fe --- /dev/null +++ b/src/IIIFPresentation/Migrator/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["Migrator/Migrator.csproj", "Migrator/"] +RUN dotnet restore "Migrator/Migrator.csproj" +COPY . . +WORKDIR "/src/Migrator" +RUN dotnet build "Migrator.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Migrator.dll"] diff --git a/src/IIIFPresentation/Migrator/Migrator.csproj b/src/IIIFPresentation/Migrator/Migrator.csproj new file mode 100644 index 00000000..6dd991e1 --- /dev/null +++ b/src/IIIFPresentation/Migrator/Migrator.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + enable + enable + Linux + + + + + .dockerignore + + + + + + + + + + + + + + + + diff --git a/src/IIIFPresentation/Migrator/Program.cs b/src/IIIFPresentation/Migrator/Program.cs new file mode 100644 index 00000000..388acb5d --- /dev/null +++ b/src/IIIFPresentation/Migrator/Program.cs @@ -0,0 +1,64 @@ +// See https://aka.ms/new-console-template for more information + +using AWS.SSM; +using Serilog; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Repository; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + +try +{ + Log.Information("Configuring IHost"); + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices(collection => { collection.AddSingleton(); }) + .ConfigureAppConfiguration((context, builder) => { builder.AddSystemsManager(context); }) + .UseSerilog() + .Build(); + + Log.Information("Executing Migrator"); + var migrator = host.Services.GetRequiredService(); + migrator.Execute(); + Log.Information("Migrator Ran"); +} +catch (Exception ex) +{ + Log.Fatal(ex, "Migrator failed"); +} +finally +{ + Log.CloseAndFlush(); +} + +class Migrator +{ + private readonly ILogger logger; + private readonly IConfiguration configuration; + + public Migrator(ILogger logger, IConfiguration configuration) + { + this.logger = logger; + this.configuration = configuration; + } + + public void Execute() + { + var connStr = configuration.GetConnectionString("PostgreSQLConnection"); + foreach (var part in connStr.Split(";")) + { + var lowered = part.ToLower(); + if (lowered.StartsWith("server") || lowered.StartsWith("database")) + { + logger.LogInformation("Got connstr part {StringPart}", lowered); + } + } + + IIIFPresentationContextConfiguration.TryRunMigrations(configuration, logger); + } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.Designer.cs new file mode 100644 index 00000000..a83fcba8 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.Designer.cs @@ -0,0 +1,108 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Repository; + +#nullable disable + +namespace Repository.Migrations +{ + [DbContext(typeof(PresentationContext))] + [Migration("20240821102322_addParentToIndex")] + partial class addParentToIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("IsStorageCollection") + .HasColumnType("boolean") + .HasColumnName("is_storage_collection"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Label") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("LockedBy") + .HasColumnType("text") + .HasColumnName("locked_by"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.Property("UsePath") + .HasColumnType("boolean") + .HasColumnName("use_path"); + + b.HasKey("Id") + .HasName("pk_collections"); + + b.HasIndex("CustomerId", "Slug", "Parent") + .IsUnique() + .HasDatabaseName("ix_collections_customer_id_slug_parent"); + + b.ToTable("collections", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.cs b/src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.cs new file mode 100644 index 00000000..6566b65f --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20240821102322_addParentToIndex.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class addParentToIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_collections_customer_id_slug", + table: "collections"); + + migrationBuilder.CreateIndex( + name: "ix_collections_customer_id_slug_parent", + table: "collections", + columns: new[] { "customer_id", "slug", "parent" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_collections_customer_id_slug_parent", + table: "collections"); + + migrationBuilder.CreateIndex( + name: "ix_collections_customer_id_slug", + table: "collections", + columns: new[] { "customer_id", "slug" }, + unique: true); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index 0785a388..8ee7b3a3 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -93,9 +93,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_collections"); - b.HasIndex("CustomerId", "Slug") + b.HasIndex("CustomerId", "Slug", "Parent") .IsUnique() - .HasDatabaseName("ix_collections_customer_id_slug"); + .HasDatabaseName("ix_collections_customer_id_slug_parent"); b.ToTable("collections", (string)null); }); diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index b6e0abf8..5b5d3089 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -29,7 +29,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { - entity.HasIndex(e => new { e.CustomerId, e.Slug }).IsUnique(); + entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); entity.Property(e => e.Label).HasColumnType("jsonb"); }); From be9e2386fc69e73b120887a7497bd31e215ff5c5 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 14:03:08 +0100 Subject: [PATCH 06/24] adding test + AddCustomer for testing with auth --- .../Integration/CreateCollectionTests.cs | 37 ++++++++++++++----- .../Integration/GetCollectionTests.cs | 20 ++++------ .../Storage/Requests/CreateCollection.cs | 2 +- .../Integration/TestAuthHandlerX.cs | 13 +++++++ 4 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 src/IIIFPresentation/Test.Helpers/Integration/TestAuthHandlerX.cs diff --git a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs index 8d72eb8a..12af9ca9 100644 --- a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs @@ -1,9 +1,6 @@ using System.Net; using System.Net.Http.Headers; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using API.Infrastructure.Requests; using API.Tests.Integration.Infrastucture; using Core.Exceptions; using Core.Response; @@ -13,11 +10,8 @@ using Models.API.Collection; using Models.API.General; using Models.Infrastucture; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using Repository; using Test.Helpers.Integration; -using JsonConverter = Newtonsoft.Json.JsonConverter; using JsonSerializer = System.Text.Json.JsonSerializer; namespace API.Tests.Integration; @@ -41,7 +35,7 @@ public CreateCollectionTests(PresentationContextFixture dbFixture, PresentationA httpClient = factory.WithConnectionString(dbFixture.ConnectionString) .CreateClient(new WebApplicationFactoryClientOptions()); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer"); + // httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer"); parent = dbContext.Collections.FirstOrDefault(x => x.CustomerId == Customer && x.Slug == string.Empty)! .Id!; @@ -64,7 +58,7 @@ public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided() }; // Act - var response = await httpClient.PostAsync($"{Customer}/collections", + var response = await httpClient.AsCustomer(Customer).PostAsync($"{Customer}/collections", new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, new MediaTypeHeaderValue("application/json"))); @@ -98,7 +92,7 @@ public async Task CreateCollection_FailsToCreateCollection_WhenDuplicateSlug() }; // Act - var response = await httpClient.PostAsync($"{Customer}/collections", + var response = await httpClient.AsCustomer(Customer).PostAsync($"{Customer}/collections", new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, new MediaTypeHeaderValue("application/json"))); @@ -109,4 +103,29 @@ public async Task CreateCollection_FailsToCreateCollection_WhenDuplicateSlug() await action.Should().ThrowAsync() .WithMessage("The collection could not be created due to a duplicate slug value"); } + + [Fact] + public async Task CreateCollection_FailsToCreateCollection_WhenCalledWithoutAuth() + { + // Arrange + var collection = new FlatCollection() + { + Behavior = new List() + { + Behavior.IsPublic, + Behavior.IsStorageCollection + }, + Label = new LanguageMap("en", new []{"test collection"}), + Slug = "first-child", + Parent = parent + }; + + // Act + var response = await httpClient.PostAsync($"{Customer}/collections", + new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, + new MediaTypeHeaderValue("application/json"))); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); + } } \ No newline at end of file diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index 4044a36d..5909d343 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -72,10 +72,9 @@ public async Task Get_RootFlat_ReturnsEntryPointHierarchical_WhenNoCsHeader() { // Arrange var requestMessage = new HttpRequestMessage(HttpMethod.Get, "1/collections/root"); - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "some-token"); - + // Act - var response = await httpClient.SendAsync(requestMessage); + var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); var collection = await response.ReadAsJsonAsync(); @@ -111,10 +110,9 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenAuthAndHeader() // Arrange var requestMessage = new HttpRequestMessage(HttpMethod.Get, "1/collections/root"); requestMessage.Headers.Add("IIIF-CS-Show-Extra", "value"); - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "some-token"); - + // Act - var response = await httpClient.SendAsync(requestMessage); + var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); var collection = await response.ReadAsJsonAsync(); @@ -135,10 +133,9 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenCalledById() // Arrange var requestMessage = new HttpRequestMessage(HttpMethod.Get, "1/collections/RootStorage"); requestMessage.Headers.Add("IIIF-CS-Show-Extra", "value"); - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "some-token"); - + // Act - var response = await httpClient.SendAsync(requestMessage); + var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); var collection = await response.ReadAsJsonAsync(); @@ -159,10 +156,9 @@ public async Task Get_ChildFlat_ReturnsEntryPointFlat_WhenCalledByChildId() // Arrange var requestMessage = new HttpRequestMessage(HttpMethod.Get, "1/collections/FirstChildCollection"); requestMessage.Headers.Add("IIIF-CS-Show-Extra", "value"); - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "some-token"); - + // Act - var response = await httpClient.SendAsync(requestMessage); + var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); var collection = await response.ReadAsJsonAsync(); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index 644fb9a8..8b242270 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -75,7 +75,7 @@ public async Task> Handle(CreateCollection re { logger.LogError(ex,"Error creating collection"); - if (ex.InnerException != null && ex.InnerException.Message.Contains("uplicate key value violates unique constraint \"ix_collections_customer_id_slug\"")) + if (ex.InnerException != null && ex.InnerException.Message.Contains("duplicate key value violates unique constraint \"ix_collections_customer_id_slug_parent\"")) { return ModifyEntityResult.Failure( $"The collection could not be created due to a duplicate slug value", WriteResult.BadRequest); diff --git a/src/IIIFPresentation/Test.Helpers/Integration/TestAuthHandlerX.cs b/src/IIIFPresentation/Test.Helpers/Integration/TestAuthHandlerX.cs new file mode 100644 index 00000000..8235d8ee --- /dev/null +++ b/src/IIIFPresentation/Test.Helpers/Integration/TestAuthHandlerX.cs @@ -0,0 +1,13 @@ +using System.Net.Http.Headers; + +namespace Test.Helpers.Integration; + +public static class TestAuthHandlerX +{ + public static HttpClient AsCustomer(this HttpClient client, int customer = 2) + { + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue($"user|{customer}"); + return client; + } +} \ No newline at end of file From b38801ad6d3a33c252ba8ddbdb34832dc8170504 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 15:00:16 +0100 Subject: [PATCH 07/24] removing java from build --- .github/workflows/run_build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml index 0563ed92..ba1f1548 100644 --- a/.github/workflows/run_build.yml +++ b/.github/workflows/run_build.yml @@ -20,11 +20,6 @@ jobs: BUILD_CONFIG: "Release" SOLUTION: "IIIFPresentation.sln" steps: - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: 'zulu' # Alternative distribution options are available. - id: checkout uses: actions/checkout@v4 - id: setup-dotnet From 547097797db62fc5aaafaa4c4f6045db961ec56c Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 15:13:03 +0100 Subject: [PATCH 08/24] change efcore version --- src/IIIFPresentation/API/API.csproj | 2 +- .../API/Features/Storage/Requests/UpdateCollection.cs | 6 ++++++ src/IIIFPresentation/Repository/Repository.csproj | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index b48105de..bb008e9b 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs new file mode 100644 index 00000000..6ecbfe40 --- /dev/null +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs @@ -0,0 +1,6 @@ +namespace API.Features.Storage.Requests; + +public class UpdateCollection +{ + +} \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/Repository.csproj b/src/IIIFPresentation/Repository/Repository.csproj index 0eb0939e..0f68824e 100644 --- a/src/IIIFPresentation/Repository/Repository.csproj +++ b/src/IIIFPresentation/Repository/Repository.csproj @@ -12,7 +12,7 @@ - + From 8f58bcaf9b82b440f84a642d3bc70681dcc119bb Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 15:31:28 +0100 Subject: [PATCH 09/24] update build + add cleanup function --- .github/workflows/run_build.yml | 2 +- .../API.Tests/Integration/CreateCollectionTests.cs | 2 ++ .../API.Tests/Integration/GetCollectionTests.cs | 2 ++ .../Test.Helpers/Integration/PresentationContextFixture.cs | 5 +++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml index ba1f1548..fbc221cc 100644 --- a/.github/workflows/run_build.yml +++ b/.github/workflows/run_build.yml @@ -33,7 +33,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any run: | dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore - dotnet dotcover test --dcReportType=HTML --filter 'Category!=Manual' --configuration $BUILD_CONFIG --no-restore --no-build --verbosity normal + dotnet test --verbosity normal build-push-api: runs-on: ubuntu-latest diff --git a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs index 12af9ca9..f79cd858 100644 --- a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs @@ -39,6 +39,8 @@ public CreateCollectionTests(PresentationContextFixture dbFixture, PresentationA parent = dbContext.Collections.FirstOrDefault(x => x.CustomerId == Customer && x.Slug == string.Empty)! .Id!; + + dbFixture.CleanUp(); } [Fact] diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index 5909d343..ed8fe562 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -20,6 +20,8 @@ public GetCollectionTests(PresentationContextFixture dbFixture, PresentationAppF { httpClient = factory.WithConnectionString(dbFixture.ConnectionString) .CreateClient(new WebApplicationFactoryClientOptions()); + + dbFixture.CleanUp(); } [Fact] diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs index b95e2038..e18be4c1 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs @@ -122,4 +122,9 @@ private void SetPropertiesFromContainer() ); DbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } + + public void CleanUp() + { + DbContext.Database.ExecuteSqlRawAsync("DELETE FROM collections WHERE id NOT IN ('RootStorage','FirstChildCollection','SecondChildCollection')"); + } } \ No newline at end of file From ce6fa2a1a4d0b871cac24784cfb0431dc763ea5f Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 15:40:26 +0100 Subject: [PATCH 10/24] re-add values to dotnet test --- .github/workflows/run_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml index fbc221cc..6d630fcb 100644 --- a/.github/workflows/run_build.yml +++ b/.github/workflows/run_build.yml @@ -33,7 +33,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any run: | dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore - dotnet test --verbosity normal + dotnet test --configuration $BUILD_CONFIG --no-restore --no-build --verbosity normal build-push-api: runs-on: ubuntu-latest From 50a9635e503820c35b46d06a8fb69a7c7e754ea1 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 15:50:06 +0100 Subject: [PATCH 11/24] move dockerfile location --- src/IIIFPresentation/Dockerfile.API => Dockerfile.API | 2 +- src/IIIFPresentation/Dockerfile.Migrator => Dockerfile.Migrator | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/IIIFPresentation/Dockerfile.API => Dockerfile.API (95%) rename src/IIIFPresentation/Dockerfile.Migrator => Dockerfile.Migrator (100%) diff --git a/src/IIIFPresentation/Dockerfile.API b/Dockerfile.API similarity index 95% rename from src/IIIFPresentation/Dockerfile.API rename to Dockerfile.API index b67fe1c9..b5fd4044 100644 --- a/src/IIIFPresentation/Dockerfile.API +++ b/Dockerfile.API @@ -6,7 +6,7 @@ COPY ["Repository/Repository.csproj", "DLCS.Repository/"] COPY ["Models/Models.csproj", "DLCS.Model/"] COPY ["Core/Core.csproj", "DLCS.Core/"] COPY ["Web/Web.csproj", "Web/"] -#COPY ["AWS/AWS.csproj", "DLCS.AWS/"] +COPY ["AWS/AWS.csproj", "DLCS.AWS/"] RUN dotnet restore "API/API.csproj" diff --git a/src/IIIFPresentation/Dockerfile.Migrator b/Dockerfile.Migrator similarity index 100% rename from src/IIIFPresentation/Dockerfile.Migrator rename to Dockerfile.Migrator From e642df90002dbb112b8e11718da24585cb59d47a Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 15:53:32 +0100 Subject: [PATCH 12/24] removing web --- Dockerfile.API | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile.API b/Dockerfile.API index b5fd4044..763b0f27 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -5,7 +5,6 @@ COPY ["API/API.csproj", "API/"] COPY ["Repository/Repository.csproj", "DLCS.Repository/"] COPY ["Models/Models.csproj", "DLCS.Model/"] COPY ["Core/Core.csproj", "DLCS.Core/"] -COPY ["Web/Web.csproj", "Web/"] COPY ["AWS/AWS.csproj", "DLCS.AWS/"] RUN dotnet restore "API/API.csproj" From fdf3ee030d47a9f558146e672d6ff0970e7c5a96 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 16:01:17 +0100 Subject: [PATCH 13/24] update sdk + image name --- .github/workflows/run_build.yml | 4 ++-- Dockerfile.API | 2 +- Dockerfile.Migrator | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml index 6d630fcb..ea07f419 100644 --- a/.github/workflows/run_build.yml +++ b/.github/workflows/run_build.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/docker-build-and-push with: - image-name: "api" + image-name: "presentation-api" dockerfile: "Dockerfile.API" context: "./src/IIIFPresentation" github-token: ${{ secrets.GITHUB_TOKEN }} @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/docker-build-and-push with: - image-name: "migrator" + image-name: "presentation-migrator" dockerfile: "Dockerfile.Migrator" context: "./src/IIIFPresentation" github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Dockerfile.API b/Dockerfile.API index 763b0f27..531e653d 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["API/API.csproj", "API/"] diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index 75835251..bc333bdc 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Utils/Migrator/Migrator.csproj", "Utils/Migrator/"] From 50ead5c4ce0ddd90c75d5fec47530bd13cf92a74 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 16:05:42 +0100 Subject: [PATCH 14/24] remove utlis --- Dockerfile.Migrator | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index bc333bdc..5200ea63 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -1,16 +1,16 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src -COPY ["Utils/Migrator/Migrator.csproj", "Utils/Migrator/"] +COPY ["Migrator/Migrator.csproj", "Migrator/"] COPY ["Repository/Repository.csproj", "DLCS.Repository/"] COPY ["DLCS.AWS/DLCS.AWS.csproj", "DLCS.AWS/"] COPY ["Models/Models.csproj", "DLCS.Model/"] COPY ["Core/Core.csproj", "DLCS.Core/"] -RUN dotnet restore "Utils/Migrator/Migrator.csproj" +RUN dotnet restore "Migrator/Migrator.csproj" COPY . . -WORKDIR "/src/Utils/Migrator" +WORKDIR "/src/Migrator" RUN dotnet build "Migrator.csproj" -c Release -o /app/build FROM build AS publish From 4ea50a211bce80dc67778219a312754b21181b43 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 16:14:13 +0100 Subject: [PATCH 15/24] rename projects + remove DLCS refs --- Dockerfile.API | 12 ++++++------ Dockerfile.Migrator | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Dockerfile.API b/Dockerfile.API index 531e653d..07db58eb 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -2,10 +2,10 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["API/API.csproj", "API/"] -COPY ["Repository/Repository.csproj", "DLCS.Repository/"] -COPY ["Models/Models.csproj", "DLCS.Model/"] -COPY ["Core/Core.csproj", "DLCS.Core/"] -COPY ["AWS/AWS.csproj", "DLCS.AWS/"] +COPY ["Repository/Repository.csproj", "Repository/"] +COPY ["Models/Models.csproj", "Model/"] +COPY ["Core/Core.csproj", "Core/"] +COPY ["AWS/AWS.csproj", "AWS/"] RUN dotnet restore "API/API.csproj" @@ -19,8 +19,8 @@ RUN dotnet publish "API.csproj" -c Release -o /app/publish FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base LABEL maintainer="Donald Gray ,Tom Crane " -LABEL org.opencontainers.image.source=https://github.com/dlcs/protagonist -LABEL org.opencontainers.image.description="HTTP API for DLCS interactions." +LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation +LABEL org.opencontainers.image.description="HTTP API for iiif presentation interactions." WORKDIR /app EXPOSE 80 diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index 5200ea63..4d9fd511 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -2,10 +2,10 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Migrator/Migrator.csproj", "Migrator/"] -COPY ["Repository/Repository.csproj", "DLCS.Repository/"] -COPY ["DLCS.AWS/DLCS.AWS.csproj", "DLCS.AWS/"] -COPY ["Models/Models.csproj", "DLCS.Model/"] -COPY ["Core/Core.csproj", "DLCS.Core/"] +COPY ["Repository/Repository.csproj", "Repository/"] +COPY ["AWS/AWS.csproj", "AWS/"] +COPY ["Models/Models.csproj", "Model/"] +COPY ["Core/Core.csproj", "Core/"] RUN dotnet restore "Migrator/Migrator.csproj" @@ -19,8 +19,8 @@ RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base LABEL maintainer="Donald Gray " -LABEL org.opencontainers.image.source=https://github.com/dlcs/protagonist -LABEL org.opencontainers.image.description="EF Migration runner for DLCS DB" +LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation +LABEL org.opencontainers.image.description="EF Migration runner for iiif presentation DB" WORKDIR /app EXPOSE 80 From c328bdbeff0283a78249cf99ac8b84a7bd55dd56 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 16:49:22 +0100 Subject: [PATCH 16/24] remove unneeded file --- .../API/Features/Storage/Requests/UpdateCollection.cs | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs deleted file mode 100644 index 6ecbfe40..00000000 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace API.Features.Storage.Requests; - -public class UpdateCollection -{ - -} \ No newline at end of file From ee49cda1a29341b2e54be560495c9ea26c7e24f5 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 21 Aug 2024 17:31:54 +0100 Subject: [PATCH 17/24] add changes pointed out by github --- .../API/Features/Storage/StorageController.cs | 19 +++++++++++++++++++ .../Validators/FlatCollectionValidator.cs | 8 ++++++++ .../Models/API/Collection/Item.cs | 4 ++-- .../Models/API/Collection/PartOf.cs | 4 ++-- .../Models/API/Collection/SeeAlso.cs | 6 +++--- .../Models/API/Collection/View.cs | 2 +- 6 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index 4a9cef80..f2a6309c 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -80,4 +80,23 @@ public async Task Post(int customerId, [FromBody] FlatCollection return await HandleUpsert(new CreateCollection(customerId, collection, GetUrlRoots())); } + + [HttpPut("collections/{collectionId}")] + [EtagCaching] + public async Task Put(int customerId, string collectionId, [FromBody] FlatCollection collection, [FromServices] FlatCollectionValidator validator) + { + if (!Authorizer.CheckAuthorized(Request)) + { + return Problem(statusCode: (int)HttpStatusCode.Forbidden); + } + + var validation = await validator.ValidateAsync(collection, policy => policy.IncludeRuleSets("update")); + + if (!validation.IsValid) + { + return this.ValidationFailed(validation); + } + + return await HandleUpsert(new UpdateCollection(customerId, collectionId, collection, GetUrlRoots())); + } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs b/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs index b9f58f04..c87707b3 100644 --- a/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs +++ b/src/IIIFPresentation/API/Features/Storage/Validators/FlatCollectionValidator.cs @@ -14,5 +14,13 @@ public FlatCollectionValidator() RuleFor(a => a.Id).Empty().WithMessage("Id cannot be set"); RuleFor(a => a.Parent).NotEmpty().WithMessage("Creating a new collection requires a parent"); }); + + RuleSet("update", () => + { + RuleFor(a => a.Created).Empty().WithMessage("Created cannot be set"); + RuleFor(a => a.Modified).Empty().WithMessage("Modified cannot be set"); + RuleFor(a => a.Id).Empty().WithMessage("Id cannot be set"); + RuleFor(a => a.Parent).NotEmpty().WithMessage("Updating a collection requires a parent to be set"); + }); } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/Item.cs b/src/IIIFPresentation/Models/API/Collection/Item.cs index f5a11df9..46f46f12 100644 --- a/src/IIIFPresentation/Models/API/Collection/Item.cs +++ b/src/IIIFPresentation/Models/API/Collection/Item.cs @@ -2,9 +2,9 @@ public class Item { - public string Id { get; set; } + public required string Id { get; set; } public PresentationType Type { get; set; } - public Dictionary> Label { get; set; } + public Dictionary>? Label { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/PartOf.cs b/src/IIIFPresentation/Models/API/Collection/PartOf.cs index c3c94710..e44f3973 100644 --- a/src/IIIFPresentation/Models/API/Collection/PartOf.cs +++ b/src/IIIFPresentation/Models/API/Collection/PartOf.cs @@ -2,9 +2,9 @@ public class PartOf { - public string Id { get; set; } + public required string Id { get; set; } public PresentationType Type { get; set; } - public Dictionary> Label { get; set; } + public Dictionary>? Label { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs index 56ed93ae..ceeb2761 100644 --- a/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs +++ b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs @@ -2,11 +2,11 @@ namespace Models.API.Collection; public class SeeAlso { - public string Id { get; set; } + public required string Id { get; set; } public PresentationType Type { get; set; } - public Dictionary> Label { get; set; } + public Dictionary>? Label { get; set; } - public List Profile { get; set; } + public List? Profile { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/View.cs b/src/IIIFPresentation/Models/API/Collection/View.cs index 405812e9..1de10104 100644 --- a/src/IIIFPresentation/Models/API/Collection/View.cs +++ b/src/IIIFPresentation/Models/API/Collection/View.cs @@ -5,7 +5,7 @@ namespace Models.API.Collection; public class View { [JsonPropertyName("@id")] - public string Id { get; set; } + public required string Id { get; set; } [JsonPropertyName("@type")] public PresentationType Type { get; set; } From 9a84ce12f9dc47796f83a318ec96ec5784bc255f Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 22 Aug 2024 09:08:42 +0100 Subject: [PATCH 18/24] remove broken collection --- .../API/Features/Storage/StorageController.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index f2a6309c..4a9cef80 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -80,23 +80,4 @@ public async Task Post(int customerId, [FromBody] FlatCollection return await HandleUpsert(new CreateCollection(customerId, collection, GetUrlRoots())); } - - [HttpPut("collections/{collectionId}")] - [EtagCaching] - public async Task Put(int customerId, string collectionId, [FromBody] FlatCollection collection, [FromServices] FlatCollectionValidator validator) - { - if (!Authorizer.CheckAuthorized(Request)) - { - return Problem(statusCode: (int)HttpStatusCode.Forbidden); - } - - var validation = await validator.ValidateAsync(collection, policy => policy.IncludeRuleSets("update")); - - if (!validation.IsValid) - { - return this.ValidationFailed(validation); - } - - return await HandleUpsert(new UpdateCollection(customerId, collectionId, collection, GetUrlRoots())); - } } \ No newline at end of file From b2f44d5ca79a4870d02bed38d4e18befb6064ac4 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 22 Aug 2024 09:56:28 +0100 Subject: [PATCH 19/24] removing GH warnings --- .../Core/Response/HttpResponseMessageX.cs | 2 +- .../Models/API/Collection/HierarchicalCollection.cs | 8 ++++---- .../Integration/PresentationContextFixture.cs | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs index 8c5612ed..a5254fb2 100644 --- a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs +++ b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs @@ -39,7 +39,7 @@ public static class HttpResponseMessageX { return serializer.Deserialize(jsonReader); } - catch (Exception exception) + catch (Exception) { return serializer.Deserialize(jsonReader); } diff --git a/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs b/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs index b1197c96..f4d63966 100644 --- a/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs @@ -5,13 +5,13 @@ namespace Models.API.Collection; public class HierarchicalCollection { [JsonPropertyName("@context")] - public string Context { get; set; } + public required string Context { get; set; } - public string Id { get; set; } + public required string Id { get; set; } public PresentationType Type { get; set; } - public Dictionary> Label { get; set; } + public Dictionary>? Label { get; set; } - public List Items { get; set; } + public List? Items { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs index e18be4c1..2d463fd0 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs @@ -4,6 +4,8 @@ using Repository; using Testcontainers.PostgreSql; +#nullable disable + namespace Test.Helpers.Integration; public class PresentationContextFixture : IAsyncLifetime From c51d633190b70ad8eeddec5c32d617e9c1ee6529 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 22 Aug 2024 10:03:50 +0100 Subject: [PATCH 20/24] second set of removal of warnings --- src/IIIFPresentation/API/Converters/UrlRoots.cs | 4 ++-- src/IIIFPresentation/API/Exceptions/ApiException.cs | 8 ++------ src/IIIFPresentation/API/Settings/ApiSettings.cs | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/IIIFPresentation/API/Converters/UrlRoots.cs b/src/IIIFPresentation/API/Converters/UrlRoots.cs index 0444f909..68263b8c 100644 --- a/src/IIIFPresentation/API/Converters/UrlRoots.cs +++ b/src/IIIFPresentation/API/Converters/UrlRoots.cs @@ -9,10 +9,10 @@ public class UrlRoots /// /// The base URI for current request - this is the full URI excluding path and query string /// - public string BaseUrl { get; set; } + public required string BaseUrl { get; set; } /// /// The base URI for image services and other public-facing resources /// - public string ResourceRoot { get; set; } + public required string ResourceRoot { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Exceptions/ApiException.cs b/src/IIIFPresentation/API/Exceptions/ApiException.cs index 100d11cf..097dbf98 100644 --- a/src/IIIFPresentation/API/Exceptions/ApiException.cs +++ b/src/IIIFPresentation/API/Exceptions/ApiException.cs @@ -7,11 +7,7 @@ public class APIException : Exception public APIException() { } - - protected APIException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - + public APIException(string? message) : base(message) { } @@ -22,5 +18,5 @@ public APIException(string? message, Exception? innerException) : base(message, public virtual int? StatusCode { get; set; } - public virtual string Label { get; set; } + public virtual string? Label { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Settings/ApiSettings.cs b/src/IIIFPresentation/API/Settings/ApiSettings.cs index 81cf602e..14dfc712 100644 --- a/src/IIIFPresentation/API/Settings/ApiSettings.cs +++ b/src/IIIFPresentation/API/Settings/ApiSettings.cs @@ -5,12 +5,12 @@ public class ApiSettings /// /// The base URI for image services and other public-facing resources /// - public Uri ResourceRoot { get; set; } + public Uri? ResourceRoot { get; set; } /// /// Page size for paged collections /// public int PageSize { get; set; } = 100; - public string PathBase { get; set; } + public string? PathBase { get; set; } } \ No newline at end of file From 71d157548fe5649e6e879f2ef4ebab283018557a Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 22 Aug 2024 10:08:39 +0100 Subject: [PATCH 21/24] fixing UrlRoot --- src/IIIFPresentation/API/Converters/UrlRoots.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IIIFPresentation/API/Converters/UrlRoots.cs b/src/IIIFPresentation/API/Converters/UrlRoots.cs index 68263b8c..d795f6fa 100644 --- a/src/IIIFPresentation/API/Converters/UrlRoots.cs +++ b/src/IIIFPresentation/API/Converters/UrlRoots.cs @@ -9,10 +9,10 @@ public class UrlRoots /// /// The base URI for current request - this is the full URI excluding path and query string /// - public required string BaseUrl { get; set; } + public string? BaseUrl { get; set; } /// /// The base URI for image services and other public-facing resources /// - public required string ResourceRoot { get; set; } + public string? ResourceRoot { get; set; } } \ No newline at end of file From e6102da26495974087c4915dcf96571258123292 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 22 Aug 2024 10:16:38 +0100 Subject: [PATCH 22/24] more changes to remove warnings --- .../Converters/CollectionConverterTests.cs | 2 ++ .../API.Tests/Integration/GetCollectionTests.cs | 2 ++ .../Storage/Requests/GetHierarchicalCollection.cs | 2 +- .../API/Infrastructure/PresentationController.cs | 2 +- src/IIIFPresentation/Migrator/Program.cs | 13 ++++++++----- .../Integration/PresentationAppFactory.cs | 2 ++ .../Integration/PresentationAppFactoryX.cs | 2 ++ 7 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs index 6c8a080f..6393d6f3 100644 --- a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs +++ b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs @@ -4,6 +4,8 @@ using Models.API.Collection; using Models.Database.Collections; +#nullable disable + namespace API.Tests.Converters; public class CollectionConverterTests diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index ed8fe562..7f6c5436 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -8,6 +8,8 @@ using Repository; using Test.Helpers.Integration; +#nullable disable + namespace API.Tests.Integration; [Trait("Category", "Integration")] diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 23b0d247..05e88c12 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -135,7 +135,7 @@ INNER JOIN var storage = await dbContext.Collections.FromSqlRaw(query).OrderBy(i => i.CustomerId) .FirstOrDefaultAsync(cancellationToken); - IQueryable items = null; + IQueryable? items = null; if (storage != null) { diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index cc43becc..4458824d 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -33,7 +33,7 @@ protected UrlRoots GetUrlRoots() return new UrlRoots { BaseUrl = Request.GetBaseUrl(), - ResourceRoot = Settings.ResourceRoot.ToString() + ResourceRoot = Settings.ResourceRoot!.ToString() }; } diff --git a/src/IIIFPresentation/Migrator/Program.cs b/src/IIIFPresentation/Migrator/Program.cs index 388acb5d..e51e7864 100644 --- a/src/IIIFPresentation/Migrator/Program.cs +++ b/src/IIIFPresentation/Migrator/Program.cs @@ -50,15 +50,18 @@ public Migrator(ILogger logger, IConfiguration configuration) public void Execute() { var connStr = configuration.GetConnectionString("PostgreSQLConnection"); - foreach (var part in connStr.Split(";")) + if (connStr != null) { - var lowered = part.ToLower(); - if (lowered.StartsWith("server") || lowered.StartsWith("database")) + foreach (var part in connStr.Split(";")) { - logger.LogInformation("Got connstr part {StringPart}", lowered); + var lowered = part.ToLower(); + if (lowered.StartsWith("server") || lowered.StartsWith("database")) + { + logger.LogInformation("Got connstr part {StringPart}", lowered); + } } } - + IIIFPresentationContextConfiguration.TryRunMigrations(configuration, logger); } } \ No newline at end of file diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactory.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactory.cs index 5780f5d6..78ec99cf 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactory.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactory.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +#nullable disable + namespace Test.Helpers.Integration; /// diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactoryX.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactoryX.cs index 1f5a5925..f8bfbea4 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactoryX.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationAppFactoryX.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc.Testing; +#nullable disable + namespace Test.Helpers.Integration; public static class PresentationAppFactoryX From c76fd41f6b8a1209867c2e8135572074bda7a952 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Fri, 23 Aug 2024 17:32:45 +0100 Subject: [PATCH 23/24] various changes for code review + move to using more of the iiif-net library --- .../actions/docker-build-and-push/action.yml | 5 -- .github/workflows/run_build.yml | 2 - Dockerfile.API | 4 +- Dockerfile.Migrator | 4 +- src/IIIFPresentation/.dockerignore | 25 ------ .../Converters/CollectionConverterTests.cs | 4 - .../Integration/CreateCollectionTests.cs | 10 +-- .../Integration/GetCollectionTests.cs | 57 ++++++++----- src/IIIFPresentation/API/API.csproj | 3 +- .../API/Converters/CollectionConverter.cs | 53 ++++++------ .../Storage/Requests/CreateCollection.cs | 12 ++- .../API/Features/Storage/StorageController.cs | 3 +- .../Infrastructure/PresentationController.cs | 2 +- src/IIIFPresentation/API/Program.cs | 6 +- .../API/Settings/ApiSettings.cs | 2 +- src/IIIFPresentation/AWS/AWS.csproj | 4 - src/IIIFPresentation/Core/Core.csproj | 5 +- .../Core/Response/HttpResponseMessageX.cs | 81 +++++++++++++------ src/IIIFPresentation/Migrator/Dockerfile | 18 ----- src/IIIFPresentation/Migrator/Migrator.csproj | 6 -- .../Models/API/Collection/FlatCollection.cs | 10 +-- .../API/Collection/HierarchicalCollection.cs | 17 ---- .../Models/API/Collection/Item.cs | 6 +- .../Models/API/Collection/PartOf.cs | 6 +- .../Models/API/Collection/SeeAlso.cs | 4 +- .../Models/API/General/Error.cs | 6 +- src/IIIFPresentation/Models/Models.csproj | 2 +- .../Repository/Repository.csproj | 2 +- .../Test.Helpers/Test.Helpers.csproj | 4 + 29 files changed, 170 insertions(+), 193 deletions(-) delete mode 100644 src/IIIFPresentation/.dockerignore delete mode 100644 src/IIIFPresentation/Migrator/Dockerfile delete mode 100644 src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs diff --git a/.github/actions/docker-build-and-push/action.yml b/.github/actions/docker-build-and-push/action.yml index 3bb92af6..94fc3521 100644 --- a/.github/actions/docker-build-and-push/action.yml +++ b/.github/actions/docker-build-and-push/action.yml @@ -20,11 +20,6 @@ runs: steps: - id: checkout uses: actions/checkout@v4 - - id: docker-setup-buildx - uses: docker/setup-buildx-action@v2 - with: - driver-opts: | - image=moby/buildkit:v0.10.6 - id: docker-meta uses: docker/metadata-action@v3 with: diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml index ea07f419..8577a781 100644 --- a/.github/workflows/run_build.yml +++ b/.github/workflows/run_build.yml @@ -29,8 +29,6 @@ jobs: - id: restore-dotnet-dependencies run: dotnet restore $SOLUTION - id: build-dotnet - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any run: | dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore dotnet test --configuration $BUILD_CONFIG --no-restore --no-build --verbosity normal diff --git a/Dockerfile.API b/Dockerfile.API index 07db58eb..3b94e5c0 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -16,9 +16,9 @@ RUN dotnet build "API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "API.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0-bullseye-slim AS base -LABEL maintainer="Donald Gray ,Tom Crane " +LABEL maintainer="Donald Gray ,Tom Crane , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation LABEL org.opencontainers.image.description="HTTP API for iiif presentation interactions." diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index 4d9fd511..45a0a8de 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -16,9 +16,9 @@ RUN dotnet build "Migrator.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0-bullseye-slim AS base -LABEL maintainer="Donald Gray " +LABEL maintainer="Donald Gray , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation LABEL org.opencontainers.image.description="EF Migration runner for iiif presentation DB" diff --git a/src/IIIFPresentation/.dockerignore b/src/IIIFPresentation/.dockerignore deleted file mode 100644 index cd967fc3..00000000 --- a/src/IIIFPresentation/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.idea -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file diff --git a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs index 6393d6f3..2db2555c 100644 --- a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs +++ b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs @@ -29,7 +29,6 @@ public void ToHierarchicalCollection_ConvertsStorageCollection() storageRoot.ToHierarchicalCollection(urlRoots, new EnumerableQuery(CreateTestItems())); // Assert hierarchicalCollection.Id.Should().Be("http://base/1"); - hierarchicalCollection.Type.Should().Be(PresentationType.Collection); hierarchicalCollection.Label.Count.Should().Be(1); hierarchicalCollection.Label["en"].Should().Contain("repository root"); hierarchicalCollection.Items.Count.Should().Be(1); @@ -46,7 +45,6 @@ public void ToHierarchicalCollection_ConvertsStorageCollectionWithFullPath() storageRoot.ToHierarchicalCollection(urlRoots, new EnumerableQuery(CreateTestItems())); // Assert hierarchicalCollection.Id.Should().Be("http://base/1/top/some-id"); - hierarchicalCollection.Type.Should().Be(PresentationType.Collection); hierarchicalCollection.Label.Count.Should().Be(1); hierarchicalCollection.Label["en"].Should().Contain("repository root"); hierarchicalCollection.Items.Count.Should().Be(1); @@ -65,7 +63,6 @@ public void ToFlatCollection_ConvertsStorageCollection() // Assert flatCollection.Id.Should().Be("http://base/1/collections/some-id"); flatCollection.PublicId.Should().Be("http://base/1"); - flatCollection.Type.Should().Be(PresentationType.Collection); flatCollection.Label.Count.Should().Be(1); flatCollection.Label["en"].Should().Contain("repository root"); flatCollection.Slug.Should().Be("root"); @@ -90,7 +87,6 @@ public void ToFlatCollection_ConvertsStorageCollection_WithFullPath() // Assert flatCollection.Id.Should().Be("http://base/1/collections/some-id"); flatCollection.PublicId.Should().Be("http://base/1/top/some-id"); - flatCollection.Type.Should().Be(PresentationType.Collection); flatCollection.Label.Count.Should().Be(1); flatCollection.Label["en"].Should().Contain("repository root"); flatCollection.Slug.Should().Be("root"); diff --git a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs index f79cd858..99309795 100644 --- a/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/CreateCollectionTests.cs @@ -2,7 +2,6 @@ using System.Net.Http.Headers; using System.Text; using API.Tests.Integration.Infrastucture; -using Core.Exceptions; using Core.Response; using FluentAssertions; using IIIF.Presentation.V3.Strings; @@ -35,8 +34,6 @@ public CreateCollectionTests(PresentationContextFixture dbFixture, PresentationA httpClient = factory.WithConnectionString(dbFixture.ConnectionString) .CreateClient(new WebApplicationFactoryClientOptions()); - // httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer"); - parent = dbContext.Collections.FirstOrDefault(x => x.CustomerId == Customer && x.Slug == string.Empty)! .Id!; @@ -98,12 +95,11 @@ public async Task CreateCollection_FailsToCreateCollection_WhenDuplicateSlug() new StringContent(JsonSerializer.Serialize(collection), Encoding.UTF8, new MediaTypeHeaderValue("application/json"))); - Func action = async () => await response.ReadAsPresentationResponseAsync(); + var error = await response.ReadAsPresentationResponseAsync(); // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - await action.Should().ThrowAsync() - .WithMessage("The collection could not be created due to a duplicate slug value"); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + error!.Detail.Should().Be("The collection could not be created due to a duplicate slug value"); } [Fact] diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index 7f6c5436..ee645e49 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -3,8 +3,12 @@ using API.Tests.Integration.Infrastucture; using Core.Response; using FluentAssertions; +using IIIF.Presentation.V3; +using IIIF.Serialisation; +using IIIF.Serialisation.Deserialisation; using Microsoft.AspNetCore.Mvc.Testing; using Models.API.Collection; +using Newtonsoft.Json; using Repository; using Test.Helpers.Integration; @@ -32,13 +36,25 @@ public async Task Get_RootHierarchical_Returns_EntryPoint() // Act var response = await httpClient.GetAsync("1"); - var collection = await response.ReadAsJsonAsync(); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - collection!.Id.Should().Be("http://localhost/1"); - collection.Items.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/first-child"); + var stuff = await response.Content.ReadAsStringAsync(); + + try + { + // var collection = stuff.FromJson(); + + var collection = await response.ReadAsIIIFJsonAsync(); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + collection!.Id.Should().Be("http://localhost/1"); + collection.Items.Count.Should().Be(1); + var firstItem = (Collection)collection.Items[0]; + firstItem.Id.Should().Be("http://localhost/1/first-child"); + } + catch (Exception ex) + { + + } } [Fact] @@ -47,13 +63,15 @@ public async Task Get_ChildHierarchical_Returns_Child() // Act var response = await httpClient.GetAsync("1/first-child"); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsIIIFJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1/first-child"); collection.Items.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/first-child/second-child"); + + var firstItem = (Collection)collection.Items[0]; + firstItem.Id.Should().Be("http://localhost/1/first-child/second-child"); } [Fact] @@ -62,13 +80,14 @@ public async Task Get_RootFlat_ReturnsEntryPointHierarchical_WhenNoAuthAndCsHead // Act var response = await httpClient.GetAsync("1/collections/root"); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsIIIFJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1"); collection.Items.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/first-child"); + var firstItem = (Collection)collection.Items[0]; + firstItem.Id.Should().Be("http://localhost/1/first-child"); } [Fact] @@ -80,13 +99,14 @@ public async Task Get_RootFlat_ReturnsEntryPointHierarchical_WhenNoCsHeader() // Act var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsIIIFJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1"); collection.Items.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/first-child"); + var firstItem = (Collection)collection.Items[0]; + firstItem.Id.Should().Be("http://localhost/1/first-child"); } [Fact] @@ -99,13 +119,14 @@ public async Task Get_RootFlat_ReturnsEntryPointHierarchical_WhenNoAuth() // Act var response = await httpClient.SendAsync(requestMessage); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsIIIFJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); collection!.Id.Should().Be("http://localhost/1"); collection.Items.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/first-child"); + var firstItem = (Collection)collection.Items[0]; + firstItem.Id.Should().Be("http://localhost/1/first-child"); } [Fact] @@ -118,7 +139,7 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenAuthAndHeader() // Act var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsPresentationJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -141,7 +162,7 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenCalledById() // Act var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsPresentationJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -164,7 +185,7 @@ public async Task Get_ChildFlat_ReturnsEntryPointFlat_WhenCalledByChildId() // Act var response = await httpClient.AsCustomer(1).SendAsync(requestMessage); - var collection = await response.ReadAsJsonAsync(); + var collection = await response.ReadAsPresentationJsonAsync(); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index bb008e9b..f06703f6 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -10,8 +10,9 @@ - + + all diff --git a/src/IIIFPresentation/API/Converters/CollectionConverter.cs b/src/IIIFPresentation/API/Converters/CollectionConverter.cs index c654a148..427d11a6 100644 --- a/src/IIIFPresentation/API/Converters/CollectionConverter.cs +++ b/src/IIIFPresentation/API/Converters/CollectionConverter.cs @@ -1,32 +1,34 @@ using API.Infrastructure.Helpers; +using IIIF.Presentation.V3; +using IIIF.Presentation.V3.Annotation; +using IIIF.Presentation.V3.Content; using Models.API.Collection; -using Models.Database.Collections; using Models.Infrastucture; namespace API.Converters; public static class CollectionConverter { - public static HierarchicalCollection ToHierarchicalCollection(this Collection dbAsset, UrlRoots urlRoots, - IQueryable? items) + public static Collection ToHierarchicalCollection(this Models.Database.Collections.Collection dbAsset, UrlRoots urlRoots, + IQueryable? items) { - return new HierarchicalCollection() + var stuff = new Collection() { Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}{(dbAsset.FullPath != null ? $"/{dbAsset.FullPath}" : "")}", Context = "http://iiif.io/api/presentation/3/context.json", Label = dbAsset.Label, - Items = items != null ? items.Select(x => new Item + Items = items != null ? items.Select(x => new Collection() { Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}/{x.FullPath}", - Label = x.Label, - Type = PresentationType.Collection - }).ToList() : new List(), - Type = PresentationType.Collection + Label = x.Label + }).ToList() : new List() }; + + return stuff; } - public static FlatCollection ToFlatCollection(this Collection dbAsset, UrlRoots urlRoots, int pageSize, - IQueryable? items) + public static FlatCollection ToFlatCollection(this Models.Database.Collections.Collection dbAsset, UrlRoots urlRoots, int pageSize, + IQueryable? items) { var itemCount = items?.Count() ?? 0; @@ -53,9 +55,9 @@ public static FlatCollection ToFlatCollection(this Collection dbAsset, UrlRoots Id = $"{urlRoots.BaseUrl}/{i.CustomerId}/collections/{i.Id}", Label = i.Label, Type = PresentationType.Collection - }).ToList() : new List(), + }).ToList() : [], - PartOf = dbAsset.Parent != null ? new List() + PartOf = dbAsset.Parent != null ? new List { new() { @@ -67,7 +69,7 @@ public static FlatCollection ToFlatCollection(this Collection dbAsset, UrlRoots TotalItems = itemCount, - View = new View() + View = new View { Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}/collections/{dbAsset.Id}?page=1&pageSize={pageSize}", Type = PresentationType.PartialCollectionView, @@ -76,29 +78,26 @@ public static FlatCollection ToFlatCollection(this Collection dbAsset, UrlRoots TotalPages = itemCount % pageSize }, - SeeAlso = new List() - { + SeeAlso = + [ new() { - Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}{(dbAsset.FullPath != null ? $"/{dbAsset.FullPath}" : "")}", + Id = + $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}{(dbAsset.FullPath != null ? $"/{dbAsset.FullPath}" : "")}", Type = PresentationType.Collection, Label = dbAsset.Label, - Profile = new List() - { - "Public" - } + Profile = ["Public"] }, + new() { - Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}{(dbAsset.FullPath != null ? $"/{dbAsset.FullPath}" : "")}/iiif", + Id = + $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}{(dbAsset.FullPath != null ? $"/{dbAsset.FullPath}" : "")}/iiif", Type = PresentationType.Collection, Label = dbAsset.Label, - Profile = new List() - { - "api-hierarchical" - } + Profile = ["api-hierarchical"] } - }, + ], Created = dbAsset.Created, Modified = dbAsset.Modified, diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index 8b242270..6f578eb6 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -73,23 +73,21 @@ public async Task> Handle(CreateCollection re } catch (DbUpdateException ex) { - logger.LogError(ex,"Error creating collection"); + logger.LogError(ex,"Error creating collection for customer {Customer} in the database", request.CustomerId); if (ex.InnerException != null && ex.InnerException.Message.Contains("duplicate key value violates unique constraint \"ix_collections_customer_id_slug_parent\"")) { return ModifyEntityResult.Failure( $"The collection could not be created due to a duplicate slug value", WriteResult.BadRequest); } - else - { - return ModifyEntityResult.Failure( - $"The collection could not be created"); - } + + return ModifyEntityResult.Failure( + $"The collection could not be created"); } return ModifyEntityResult.Success( collection.ToFlatCollection(request.UrlRoots, settings.PageSize, - new EnumerableQuery(new List())), // there can be no items attached to this as it's just been created + new EnumerableQuery(Enumerable.Empty())), // there can be no items attached to this, as it's just been created WriteResult.Created); } diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index 4a9cef80..b04c43bd 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using FluentValidation; using Models.API.Collection; +using NuGet.Protocol; namespace API.Features.Storage; @@ -32,7 +33,7 @@ public async Task GetHierarchicalRootCollection(int customerId) if (storageRoot.Collection == null) return NotFound(); - return Ok( storageRoot.Collection.ToHierarchicalCollection(GetUrlRoots(), storageRoot.Items)); + return Ok(storageRoot.Collection.ToHierarchicalCollection(GetUrlRoots(), storageRoot.Items)); } [HttpGet("{*slug}")] diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index 4458824d..cc43becc 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -33,7 +33,7 @@ protected UrlRoots GetUrlRoots() return new UrlRoots { BaseUrl = Request.GetBaseUrl(), - ResourceRoot = Settings.ResourceRoot!.ToString() + ResourceRoot = Settings.ResourceRoot.ToString() }; } diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 5b5b2c38..23879e8a 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -2,6 +2,7 @@ using API.Features.Storage.Validators; using API.Infrastructure; using API.Settings; +using Newtonsoft.Json; using Repository; using Serilog; @@ -15,10 +16,9 @@ builder.Services.AddSerilog(lc => lc .ReadFrom.Configuration(builder.Configuration)); -builder.Services.AddControllers().AddJsonOptions(opt => +builder.Services.AddControllers().AddNewtonsoftJson(options => { - opt.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); builder.Services.AddScoped(); diff --git a/src/IIIFPresentation/API/Settings/ApiSettings.cs b/src/IIIFPresentation/API/Settings/ApiSettings.cs index 14dfc712..e61c5536 100644 --- a/src/IIIFPresentation/API/Settings/ApiSettings.cs +++ b/src/IIIFPresentation/API/Settings/ApiSettings.cs @@ -5,7 +5,7 @@ public class ApiSettings /// /// The base URI for image services and other public-facing resources /// - public Uri? ResourceRoot { get; set; } + public required Uri ResourceRoot { get; set; } /// /// Page size for paged collections diff --git a/src/IIIFPresentation/AWS/AWS.csproj b/src/IIIFPresentation/AWS/AWS.csproj index 308f512d..9699d05a 100644 --- a/src/IIIFPresentation/AWS/AWS.csproj +++ b/src/IIIFPresentation/AWS/AWS.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/src/IIIFPresentation/Core/Core.csproj b/src/IIIFPresentation/Core/Core.csproj index 38f44843..826a5366 100644 --- a/src/IIIFPresentation/Core/Core.csproj +++ b/src/IIIFPresentation/Core/Core.csproj @@ -7,11 +7,8 @@ + - - - - diff --git a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs index a5254fb2..62cdc21c 100644 --- a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs +++ b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs @@ -1,5 +1,6 @@ using Core.Exceptions; -using Models.API.General; +using IIIF; +using IIIF.Serialisation; using Newtonsoft.Json; namespace Core.Response; @@ -14,15 +15,35 @@ public static class HttpResponseMessageX /// /// Type to convert response to /// Converted Http response. - public static async Task ReadAsJsonAsync(this HttpResponseMessage response, - bool ensureSuccess = true, JsonSerializerSettings? settings = null) + public static async Task ReadAsIIIFJsonAsync(this HttpResponseMessage response, + bool ensureSuccess = true, JsonSerializerSettings? settings = null) where T : JsonLdBase { if (ensureSuccess) response.EnsureSuccessStatusCode(); if (!response.IsJsonResponse()) return default; var contentStream = await response.Content.ReadAsStreamAsync(); + + return contentStream.FromJsonStream(); + } + + /// + /// Read HttpResponseMessage as JSON using Newtonsoft for conversion. + /// + /// object + /// If true, will validate that the response is a 2xx. + /// + /// Type to convert response to + /// Converted Http response. + public static async Task ReadAsPresentationJsonAsync(this HttpResponseMessage response, + bool ensureSuccess = true, JsonSerializerSettings? settings = null) + { + if (ensureSuccess) response.EnsureSuccessStatusCode(); + + if (!response.IsJsonResponse()) return default; + var contentStream = await response.Content.ReadAsStreamAsync(); + using var streamReader = new StreamReader(contentStream); using var jsonReader = new JsonTextReader(streamReader); @@ -34,15 +55,8 @@ public static class HttpResponseMessageX serializer.ContractResolver = settings.ContractResolver; } serializer.NullValueHandling = settings.NullValueHandling; - - try - { - return serializer.Deserialize(jsonReader); - } - catch (Exception) - { - return serializer.Deserialize(jsonReader); - } + + return serializer.Deserialize(jsonReader); } /// @@ -57,6 +71,24 @@ public static bool IsJsonResponse(this HttpResponseMessage response) return mediaType != null && mediaType.Contains("json"); } + public static async Task ReadAsIIIFResponseAsync(this HttpResponseMessage response, + JsonSerializerSettings? settings = null) where T : JsonLdBase + { + if ((int)response.StatusCode < 400) + { + return await response.ReadWithIIIFContext(true, settings); + } + + try + { + return await response.ReadAsIIIFJsonAsync(false, settings); + } + catch (Exception ex) + { + throw new PresentationException("Could not convert response JSON to error", ex); + } + } + public static async Task ReadAsPresentationResponseAsync(this HttpResponseMessage response, JsonSerializerSettings? settings = null) { @@ -64,31 +96,32 @@ public static bool IsJsonResponse(this HttpResponseMessage response) { return await response.ReadWithContext(true, settings); } - - Error? error; + try { - error = await response.ReadAsJsonAsync(false, settings); + return await response.ReadAsPresentationJsonAsync(false, settings); } catch (Exception ex) { - throw new PresentationException("Could not find a Hydra error in response", ex); - } - - if (error != null) - { - throw new PresentationException(error.Detail); + throw new PresentationException("Could not convert response JSON to error", ex); } - - throw new PresentationException("Unable to process error condition"); } + private static async Task ReadWithIIIFContext( + this HttpResponseMessage response, + bool ensureSuccess, + JsonSerializerSettings? settings) where T : JsonLdBase + { + var json = await response.ReadAsIIIFJsonAsync(ensureSuccess, settings ?? new JsonSerializerSettings()); + return json; + } + private static async Task ReadWithContext( this HttpResponseMessage response, bool ensureSuccess, JsonSerializerSettings? settings) { - var json = await response.ReadAsJsonAsync(ensureSuccess, settings); + var json = await response.ReadAsPresentationJsonAsync(ensureSuccess, settings ?? new JsonSerializerSettings()); return json; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Migrator/Dockerfile b/src/IIIFPresentation/Migrator/Dockerfile deleted file mode 100644 index 412dd4fe..00000000 --- a/src/IIIFPresentation/Migrator/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src -COPY ["Migrator/Migrator.csproj", "Migrator/"] -RUN dotnet restore "Migrator/Migrator.csproj" -COPY . . -WORKDIR "/src/Migrator" -RUN dotnet build "Migrator.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Migrator.dll"] diff --git a/src/IIIFPresentation/Migrator/Migrator.csproj b/src/IIIFPresentation/Migrator/Migrator.csproj index 6dd991e1..4ad62e42 100644 --- a/src/IIIFPresentation/Migrator/Migrator.csproj +++ b/src/IIIFPresentation/Migrator/Migrator.csproj @@ -8,12 +8,6 @@ Linux - - - .dockerignore - - - diff --git a/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs b/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs index 9cbe8433..8e12f1ed 100644 --- a/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs @@ -1,11 +1,11 @@ -using System.Text.Json.Serialization; -using IIIF.Presentation.V3.Strings; +using IIIF.Presentation.V3.Strings; +using Newtonsoft.Json; namespace Models.API.Collection; public class FlatCollection { - [JsonPropertyName("@context")] + [JsonProperty("@context")] public List? Context { get; set; } public string? Id { get; set; } @@ -16,9 +16,9 @@ public class FlatCollection public List Behavior { get; set; } = new (); - public LanguageMap Label { get; set; } = null!; + public required LanguageMap Label { get; set; } - public string Slug { get; set; } = null!; + public required string Slug { get; set; } public string? Parent { get; set; } diff --git a/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs b/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs deleted file mode 100644 index f4d63966..00000000 --- a/src/IIIFPresentation/Models/API/Collection/HierarchicalCollection.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Models.API.Collection; - -public class HierarchicalCollection -{ - [JsonPropertyName("@context")] - public required string Context { get; set; } - - public required string Id { get; set; } - - public PresentationType Type { get; set; } - - public Dictionary>? Label { get; set; } - - public List? Items { get; set; } -} \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/Item.cs b/src/IIIFPresentation/Models/API/Collection/Item.cs index 46f46f12..1edd3cf6 100644 --- a/src/IIIFPresentation/Models/API/Collection/Item.cs +++ b/src/IIIFPresentation/Models/API/Collection/Item.cs @@ -1,4 +1,6 @@ -namespace Models.API.Collection; +using IIIF.Presentation.V3.Strings; + +namespace Models.API.Collection; public class Item { @@ -6,5 +8,5 @@ public class Item public PresentationType Type { get; set; } - public Dictionary>? Label { get; set; } + public LanguageMap? Label { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/PartOf.cs b/src/IIIFPresentation/Models/API/Collection/PartOf.cs index e44f3973..8c3e53ae 100644 --- a/src/IIIFPresentation/Models/API/Collection/PartOf.cs +++ b/src/IIIFPresentation/Models/API/Collection/PartOf.cs @@ -1,4 +1,6 @@ -namespace Models.API.Collection; +using IIIF.Presentation.V3.Strings; + +namespace Models.API.Collection; public class PartOf { @@ -6,5 +8,5 @@ public class PartOf public PresentationType Type { get; set; } - public Dictionary>? Label { get; set; } + public LanguageMap? Label { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs index ceeb2761..596637e5 100644 --- a/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs +++ b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs @@ -1,3 +1,5 @@ +using IIIF.Presentation.V3.Strings; + namespace Models.API.Collection; public class SeeAlso @@ -6,7 +8,7 @@ public class SeeAlso public PresentationType Type { get; set; } - public Dictionary>? Label { get; set; } + public LanguageMap? Label { get; set; } public List? Profile { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/General/Error.cs b/src/IIIFPresentation/Models/API/General/Error.cs index 5d90d165..84475bb5 100644 --- a/src/IIIFPresentation/Models/API/General/Error.cs +++ b/src/IIIFPresentation/Models/API/General/Error.cs @@ -1,6 +1,8 @@ -namespace Models.API.General; +using IIIF; -public class Error +namespace Models.API.General; + +public class Error : JsonLdBase { public string Type => "Error"; diff --git a/src/IIIFPresentation/Models/Models.csproj b/src/IIIFPresentation/Models/Models.csproj index 5685f78b..1cae4226 100644 --- a/src/IIIFPresentation/Models/Models.csproj +++ b/src/IIIFPresentation/Models/Models.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/IIIFPresentation/Repository/Repository.csproj b/src/IIIFPresentation/Repository/Repository.csproj index 0f68824e..13426bb3 100644 --- a/src/IIIFPresentation/Repository/Repository.csproj +++ b/src/IIIFPresentation/Repository/Repository.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj b/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj index ee3cc6af..2e4bd920 100644 --- a/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj +++ b/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj @@ -26,4 +26,8 @@ + + + + From 846079cd199cdbbf24bf77ec71bea1591a39c26c Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Fri, 23 Aug 2024 17:37:35 +0100 Subject: [PATCH 24/24] remove bullseye-slim --- Dockerfile.API | 2 +- Dockerfile.Migrator | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.API b/Dockerfile.API index 3b94e5c0..7357d4bc 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -16,7 +16,7 @@ RUN dotnet build "API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "API.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:8.0-bullseye-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base LABEL maintainer="Donald Gray ,Tom Crane , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index 45a0a8de..66b75ec2 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -16,7 +16,7 @@ RUN dotnet build "Migrator.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:8.0-bullseye-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base LABEL maintainer="Donald Gray , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation