diff --git a/src/IIIFPresentation/API.Tests/Features/Manifest/Validators/PresentationManifestValidatorTests.cs b/src/IIIFPresentation/API.Tests/Features/Manifest/Validators/PresentationManifestValidatorTests.cs index 2899547a..8ed0e3be 100644 --- a/src/IIIFPresentation/API.Tests/Features/Manifest/Validators/PresentationManifestValidatorTests.cs +++ b/src/IIIFPresentation/API.Tests/Features/Manifest/Validators/PresentationManifestValidatorTests.cs @@ -1,12 +1,24 @@ using API.Features.Manifest.Validators; +using API.Settings; +using AWS.Settings; +using DLCS; using FluentValidation.TestHelper; +using IIIF.Presentation.V3; +using Microsoft.Extensions.Options; using Models.API.Manifest; namespace API.Tests.Features.Manifest.Validators; public class PresentationManifestValidatorTests { - private readonly PresentationManifestValidator sut = new(); + private readonly PresentationManifestValidator sut = new(Options.Create(new ApiSettings() + { + AWS = new AWSSettings(), + DLCS = new DlcsSettings + { + ApiUri = new Uri("https://localhost") + } + })); [Theory] [InlineData(null)] @@ -29,4 +41,70 @@ public void Parent_Required(string? parent) var result = sut.TestValidate(manifest); result.ShouldHaveValidationErrorFor(m => m.Parent); } -} \ No newline at end of file + + [Fact] + public void CanvasPaintingAndItems_Manifest_ErrorWhenDefaultSettings() + { + var manifest = new PresentationManifest + { + Items = new List() + { + new () + { + Id = "someId", + } + }, + PaintedResources = new List() + { + new () + { + CanvasPainting = new CanvasPainting() + { + CanvasId = "someCanvasId" + } + } + }, + }; + + var result = sut.TestValidate(manifest); + result.ShouldHaveValidationErrorFor(m => m.Items); + } + + [Fact] + public void CanvasPaintingAndItems_Manifest_NoErrorWhenSettings() + { + var sutAllowedItemsAndPaintedResource = new PresentationManifestValidator(Options.Create(new ApiSettings() + { + AWS = new AWSSettings(), + IgnorePaintedResourcesWithItems = true, + DLCS = new DlcsSettings + { + ApiUri = new Uri("https://localhost") + } + })); + + var manifest = new PresentationManifest + { + Items = new List() + { + new () + { + Id = "someId", + } + }, + PaintedResources = new List() + { + new () + { + CanvasPainting = new CanvasPainting() + { + CanvasId = "someCanvasId" + } + } + }, + }; + + var result = sutAllowedItemsAndPaintedResource.TestValidate(manifest); + result.ShouldNotHaveValidationErrorFor(m => m.Items); + } +} diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyManifestCreateTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyManifestCreateTests.cs index 74390ee1..fdfb1d03 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyManifestCreateTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyManifestCreateTests.cs @@ -10,6 +10,7 @@ using DLCS.Exceptions; using DLCS.Models; using FakeItEasy; +using IIIF.Presentation.V3.Annotation; using IIIF.Presentation.V3.Strings; using IIIF.Serialisation; using Microsoft.EntityFrameworkCore; @@ -17,6 +18,7 @@ using Models.API.General; using Models.API.Manifest; using Models.Database.General; +using Models.Infrastucture; using Repository; using Test.Helpers; using Test.Helpers.Helpers; @@ -1235,21 +1237,42 @@ public async Task CreateManifest_BadRequest_WhenItemsAndPaintedResourceFilled() var manifest = new PresentationManifest { Parent = $"http://localhost/1/collections/{RootCollection.Id}", + Behavior = [ + Behavior.IsPublic + ], Slug = slug, - Items = new List() - { - new () + Items = + [ + new Canvas { - Id = "https://iiif.example/manifest.json", + Id = "https://iiif.example/manifestFromItems.json", + Items = + [ + new AnnotationPage + { + Id = "https://iiif.example/manifestFromItemsAnnotationPage.json", + Items = + [ + new PaintingAnnotation + { + Id = "https://iiif.example/manifestFromItemsPaintingAnnotation.json", + Body = new Canvas + { + Id = "https://iiif.example/manifestFromItemsCanvasBody.json" + } + } + ] + } + ] } - }, + ], PaintedResources = new List() { - new PaintedResource() + new () { CanvasPainting = new CanvasPainting() { - CanvasId = "https://iiif.example/manifest.json" + CanvasId = "https://iiif.example/manifestFromPainted.json" } } } @@ -1262,9 +1285,12 @@ public async Task CreateManifest_BadRequest_WhenItemsAndPaintedResourceFilled() var response = await httpClient.AsCustomer().SendAsync(requestMessage); // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - var error = await response.ReadAsPresentationResponseAsync(); - error!.Detail.Should().Be("The properties \"items\" and \"paintedResource\" cannot be used at the same time"); - error.ErrorTypeUri.Should().Be("http://localhost/errors/ModifyCollectionType/ValidationFailed"); + response.StatusCode.Should().Be(HttpStatusCode.Created); + + var presentationManifest = await response.ReadAsPresentationResponseAsync(); + presentationManifest.PaintedResources.Count.Should().Be(1); + presentationManifest.PaintedResources.First().CanvasPainting.CanvasOriginalId.Should() + .Be("https://iiif.example/manifestFromItems.json"); + presentationManifest.Items.Count.Should().Be(1); } } diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyManifestUpdateTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyManifestUpdateTests.cs index 9e6b321c..b14b59e7 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyManifestUpdateTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyManifestUpdateTests.cs @@ -6,6 +6,7 @@ using API.Tests.Integration.Infrastructure; using Core.Response; using IIIF.Presentation.V3; +using IIIF.Presentation.V3.Annotation; using IIIF.Presentation.V3.Strings; using IIIF.Serialisation; using Microsoft.EntityFrameworkCore; @@ -440,35 +441,52 @@ private void SetCorrectEtag(HttpRequestMessage requestMessage, Manifest dbManife } [Fact] - public async Task PutFlatId_Update_BadRequest_WhenItemsAndPaintedResourceFilled() + public async Task PutFlatId_Update_IgnoresPaintedResources_WhenItemsAndPaintedResourceFilled() { // Arrange var createdDate = DateTime.UtcNow.AddDays(-1); var dbManifest = (await dbContext.Manifests.AddTestManifest(createdDate: createdDate)).Entity; await dbContext.SaveChangesAsync(); - var parent = $"http://localhost/{Customer}/collections/{RootCollection.Id}"; var slug = $"changed_{dbManifest.Hierarchy.Single().Slug}"; var manifest = new PresentationManifest { Parent = $"http://localhost/1/collections/{RootCollection.Id}", Slug = slug, - Items = new List() - { - new () + Items = + [ + new Canvas { - Id = "https://iiif.example/manifest.json", + Id = "https://iiif.example/manifestFromItems.json", + Items = + [ + new AnnotationPage + { + Id = "https://iiif.example/manifestFromItemsAnnotationPage.json", + Items = + [ + new PaintingAnnotation + { + Id = "https://iiif.example/manifestFromItemsPaintingAnnotation.json", + Body = new Canvas + { + Id = "https://iiif.example/manifestFromItemsCanvasBody.json" + } + } + ] + } + ] } - }, - PaintedResources = new List() - { - new PaintedResource() + ], + PaintedResources = + [ + new PaintedResource { - CanvasPainting = new CanvasPainting() + CanvasPainting = new CanvasPainting { CanvasId = "https://iiif.example/manifest.json" } } - } + ] }; var requestMessage = @@ -480,9 +498,12 @@ public async Task PutFlatId_Update_BadRequest_WhenItemsAndPaintedResourceFilled( var response = await httpClient.AsCustomer().SendAsync(requestMessage); // Assert - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - var error = await response.ReadAsPresentationResponseAsync(); - error!.Detail.Should().Be("The properties \"items\" and \"paintedResource\" cannot be used at the same time"); - error.ErrorTypeUri.Should().Be("http://localhost/errors/ModifyCollectionType/ValidationFailed"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var presentationManifest = await response.ReadAsPresentationResponseAsync(); + presentationManifest.PaintedResources.Count.Should().Be(1); + presentationManifest.PaintedResources.First().CanvasPainting.CanvasOriginalId.Should() + .Be("https://iiif.example/manifestFromItems.json"); + presentationManifest.Items.Count.Should().Be(1); } } diff --git a/src/IIIFPresentation/API.Tests/appsettings.Testing.json b/src/IIIFPresentation/API.Tests/appsettings.Testing.json index 40981dc3..7aa9ca8c 100644 --- a/src/IIIFPresentation/API.Tests/appsettings.Testing.json +++ b/src/IIIFPresentation/API.Tests/appsettings.Testing.json @@ -3,6 +3,7 @@ "ResourceRoot": "https://localhost:7230", "PageSize": 20, "MaxPageSize": 100, + "IgnorePaintedResourcesWithItems": true, "AWS": { "S3": { "StorageBucket": "presentation-storage" diff --git a/src/IIIFPresentation/API/Features/Manifest/ManifestService.cs b/src/IIIFPresentation/API/Features/Manifest/ManifestService.cs index 957e4389..8b3d9d78 100644 --- a/src/IIIFPresentation/API/Features/Manifest/ManifestService.cs +++ b/src/IIIFPresentation/API/Features/Manifest/ManifestService.cs @@ -7,10 +7,10 @@ using API.Infrastructure.Helpers; using API.Infrastructure.IdGenerator; using API.Infrastructure.Validation; +using API.Settings; using Core; using Core.Auth; using Core.Helpers; -using DLCS; using DLCS.API; using DLCS.Exceptions; using IIIF.Serialisation; @@ -19,11 +19,8 @@ using Models.API.Manifest; using Models.Database.Collections; using Models.Database.General; -using Newtonsoft.Json.Linq; using Repository; using Repository.Helpers; -using Batch = DLCS.Models.Batch; -using CanvasPainting = Models.Database.CanvasPainting; using Collection = Models.Database.Collections.Collection; using DbManifest = Models.Database.Collections.Manifest; using PresUpdateResult = API.Infrastructure.Requests.ModifyEntityResult; @@ -59,10 +56,13 @@ public class ManifestService( IIIFS3Service iiifS3, IETagManager eTagManager, CanvasPaintingResolver canvasPaintingResolver, + IOptions options, IPathGenerator pathGenerator, IDlcsApiClient dlcsApiClient, ILogger logger) { + private readonly ApiSettings settings = options.Value; + /// /// Create or update full manifest, using details provided in request object /// @@ -123,6 +123,13 @@ private async Task CreateInternal(WriteManifestRequest request var (parentErrors, parentCollection) = await TryGetParent(request, cancellationToken); if (parentErrors != null) return parentErrors; + // can't have both items and painted resources, so items takes precedence + if (settings.IgnorePaintedResourcesWithItems && !request.PresentationManifest.Items.IsNullOrEmpty() && + !request.PresentationManifest.PaintedResources.IsNullOrEmpty()) + { + request.PresentationManifest.PaintedResources = null; + } + using (logger.BeginScope("Creating Manifest for Customer {CustomerId}", request.CustomerId)) { var (error, dbManifest) = diff --git a/src/IIIFPresentation/API/Features/Manifest/Validators/PresentationManifestValidator.cs b/src/IIIFPresentation/API/Features/Manifest/Validators/PresentationManifestValidator.cs index 34b78717..f15c0659 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Validators/PresentationManifestValidator.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Validators/PresentationManifestValidator.cs @@ -1,21 +1,28 @@ using API.Infrastructure.Validation; +using API.Settings; using Core.Helpers; +using DLCS; using FluentValidation; +using Microsoft.Extensions.Options; using Models.API.Manifest; namespace API.Features.Manifest.Validators; public class PresentationManifestValidator : AbstractValidator { - public PresentationManifestValidator() + public PresentationManifestValidator(IOptions options) { + var settings = options.Value; + RuleFor(f => f.Parent).NotEmpty().WithMessage("Requires a 'parent' to be set"); RuleFor(f => f.Slug).NotEmpty().WithMessage("Requires a 'slug' to be set") .Must(slug => !SpecConstants.ProhibitedSlugs.Contains(slug!)) .WithMessage("'slug' cannot be one of prohibited terms: '{PropertyValue}'"); - RuleFor(f => f.Items).Empty() - .When(f => !f.PaintedResources.IsNullOrEmpty()) - .WithMessage("The properties \"items\" and \"paintedResource\" cannot be used at the same time"); - + if (!settings.IgnorePaintedResourcesWithItems) + { + RuleFor(f => f.Items).Empty() + .When(f => !f.PaintedResources.IsNullOrEmpty()) + .WithMessage("The properties \"items\" and \"paintedResource\" cannot be used at the same time"); + } } } diff --git a/src/IIIFPresentation/API/Settings/ApiSettings.cs b/src/IIIFPresentation/API/Settings/ApiSettings.cs index 322a690d..36637479 100644 --- a/src/IIIFPresentation/API/Settings/ApiSettings.cs +++ b/src/IIIFPresentation/API/Settings/ApiSettings.cs @@ -17,7 +17,12 @@ public class ApiSettings public string? PathBase { get; set; } + /// + /// Whether painted resources should be ignored when there are also items + /// + public bool IgnorePaintedResourcesWithItems { get; set; } + public required AWSSettings AWS { get; set; } public required DlcsSettings DLCS { get; set; } -} \ No newline at end of file +}