From d91cfbd008ac47878b74071c61d4d8ef0491deaf Mon Sep 17 00:00:00 2001 From: griffri Date: Tue, 13 Feb 2024 13:43:33 +0000 Subject: [PATCH 01/35] Add HydraDeliveryChannelPolicyValidator, DeliveryChannelPolicy Hydra model, basic DeliveryChannelPoliciesController --- .../DeliveryChannelPoliciesController.cs | 109 ++++++++++++++++++ .../HydraDeliveryChannelPolicyValidator.cs | 40 +++++++ .../DLCS.HydraModel/DeliveryChannelPolicy.cs | 62 ++++++++++ 3 files changed, 211 insertions(+) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs create mode 100644 src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs new file mode 100644 index 000000000..8df8a98ab --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -0,0 +1,109 @@ + +using API.Features.DeliveryChannelPolicies.Validation; +using API.Infrastructure; +using API.Settings; +using DLCS.HydraModel; +using FluentValidation; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace API.Features.DeliveryChannelPolicies; + +/// +/// DLCS REST API Operations for delivery channel policies +/// +[Route("/customers/{customerId}/deliveryChannelPolicies")] +[ApiController] +public class DeliveryChannelPoliciesController : HydraController +{ + public DeliveryChannelPoliciesController( + IMediator mediator, + IOptions options) : base(options.Value, mediator) + { + } + + [HttpGet] + public async Task GetPolicyCollections(int customerId) + { + throw new NotImplementedException(); + } + + [HttpGet("{channelId}")] + public async Task GetPolicyCollection(int customerId, string channelId) + { + throw new NotImplementedException(); + } + + [HttpPost("{channelId}")] + public async Task PostPolicy( + [FromRoute] int customerId, + [FromRoute] string channelId, + [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, + [FromServices] HydraDeliveryChannelPolicyValidator validator, + CancellationToken cancellationToken) + { + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, + policy => policy.IncludeRuleSets("default", "post"), cancellationToken); + if (!validationResult.IsValid) + { + return this.ValidationFailed(validationResult); + } + + throw new NotImplementedException(); + } + + [HttpGet("{channelId}/{policyId}")] + [ProducesResponseType(200, Type = typeof(DLCS.HydraModel.DeliveryChannelPolicy))] + public async Task GetPolicy( + [FromRoute] int customerId, + [FromRoute] string channelId, + [FromRoute] string policyId) + { + throw new NotImplementedException(); + } + + [HttpPost("{channelId}/{policyId}")] + public async Task PutPolicy( + [FromRoute] int customerId, + [FromRoute] string channelId, + [FromRoute] string policyId, + [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, + [FromServices] HydraDeliveryChannelPolicyValidator validator, + CancellationToken cancellationToken) + { + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, + policy => policy.IncludeRuleSets("default", "put"), cancellationToken); + if (!validationResult.IsValid) + { + return this.ValidationFailed(validationResult); + } + + throw new NotImplementedException(); + } + + [HttpPatch("{channelId}/{policyId}")] + public async Task PatchPolicy( + [FromRoute] int customerId, + [FromRoute] string channelId, + [FromRoute] string policyId, + [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, + [FromServices] HydraDeliveryChannelPolicyValidator validator, + CancellationToken cancellationToken) + { + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, + policy => policy.IncludeRuleSets("default", "patch"), cancellationToken); + if (!validationResult.IsValid) + { + return this.ValidationFailed(validationResult); + } + + throw new NotImplementedException(); + } + + [HttpDelete("{channelId}/{policyId}")] + public async Task DeletePolicy(int customerId, string channelId, string policyId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs new file mode 100644 index 000000000..f229c1a15 --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -0,0 +1,40 @@ +using FluentValidation; + +namespace API.Features.DeliveryChannelPolicies.Validation; + +/// +/// Validator for model sent to POST /deliveryChannelPolicies and PUT/PATCH /deliveryChannelPolicies/{id} +/// +public class HydraDeliveryChannelPolicyValidator : AbstractValidator +{ + private readonly string[] allowedDeliveryChannels = {"iiif-img", "iiif-av", "thumbs"}; + + public HydraDeliveryChannelPolicyValidator() + { + RuleFor(p => p.Id) + .Empty() + .WithMessage(p => $"DLCS must allocate named origin strategy id, but id {p.Id} was supplied"); + RuleSet("post", () => + { + RuleFor(c => c.Channel) + .Empty().WithMessage("'name' is required"); + }); + RuleSet("put", () => + { + RuleFor(c => c.Channel) + .NotEmpty().WithMessage("'name' is not permitted"); + }); + RuleSet("patch", () => + { + RuleFor(c => c.Channel) + .Empty().WithMessage("'Name' cannot be modified in a PATCH operation"); + RuleFor(c => c.Channel) + .Empty().WithMessage("'Channel' cannot be modified in a PATCH operation"); + }); + RuleFor(p => p.Channel) + .Must(c => allowedDeliveryChannels.Contains(c)) + .WithMessage(p => $"'{p.Channel}' is not a supported delivery channel"); + RuleFor(p => p.PolicyModified) + .Empty().WithMessage(c => $"'policyModified' is not permitted"); + } +} \ No newline at end of file diff --git a/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs b/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs new file mode 100644 index 000000000..1ab4d396e --- /dev/null +++ b/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs @@ -0,0 +1,62 @@ +using System; +using Hydra; +using Hydra.Model; +using Newtonsoft.Json; + +namespace DLCS.HydraModel; + +[HydraClass(typeof (DeliveryChannelPolicyClass), + Description = "A policy for a delivery channel.", + UriTemplate = "/customers/{0}/deliveryChannelPolicies/{1}/{2}")] +public class DeliveryChannelPolicy : DlcsResource +{ + public DeliveryChannelPolicy() + { + + } + + public DeliveryChannelPolicy(string baseUrl) + { + Init(baseUrl, false); + } + + [RdfProperty(Description = "The URL-friendly name of this delivery channel policy.", + Range = Names.XmlSchema.String, ReadOnly = false, WriteOnly = false)] + [JsonProperty(Order = 10, PropertyName = "name")] + public string? Name { get; set; } + + [RdfProperty(Description = "The display name of this delivery channel policy", + Range = Names.XmlSchema.String, ReadOnly = false, WriteOnly = false)] + [JsonProperty(Order = 11, PropertyName = "displayName")] + public string? DisplayName { get; set; } + + [RdfProperty(Description = "The delivery channel this policy is for.", + Range = Names.XmlSchema.String, ReadOnly = false, WriteOnly = false)] + [JsonProperty(Order = 12, PropertyName = "channel")] + public string? Channel { get; set; } + + [RdfProperty(Description = "A JSON object containing configuration for the specified delivery channel - see the DeliveryChannels topic.", + Range = Names.XmlSchema.String, ReadOnly = false, WriteOnly = false)] + [JsonProperty(Order = 13, PropertyName = "policyData")] + public string? PolicyData { get; set; } + + [RdfProperty(Description = "The date this policy was last modified.", + Range = Names.XmlSchema.DateTime, ReadOnly = true, WriteOnly = false)] + [JsonProperty(Order = 14, PropertyName = "policyModified")] + public DateTime? PolicyModified { get; set; } +} + +public class DeliveryChannelPolicyClass: Class +{ + public DeliveryChannelPolicyClass() + { + BootstrapViaReflection(typeof(DeliveryChannelPolicy)); + } + + public override void DefineOperations() + { + SupportedOperations = CommonOperations.GetStandardResourceOperations( + "_:customer_deliveryChannelPolicy_", "Delivery Channel Policy", Id, + "GET", "POST", "PUT", "PATCH", "DELETE"); + } +} \ No newline at end of file From d567b656d0a38d31ac1c5acc5fc1bcd6f535bfd3 Mon Sep 17 00:00:00 2001 From: griffri Date: Tue, 13 Feb 2024 17:21:58 +0000 Subject: [PATCH 02/35] Add DeliveryChannelPolicy converter class, add GetDeliveryChannelPolicy request, implement GetDeliveryChannelPolicy in policies controller --- .../DeliveryChannelPolicyConverter.cs | 18 +++++ .../DeliveryChannelPoliciesController.cs | 65 ++++++++++++------- .../Requests/GetDeliveryChannelPolicy.cs | 46 +++++++++++++ 3 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs new file mode 100644 index 000000000..1939d263b --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs @@ -0,0 +1,18 @@ +namespace API.Features.DeliveryChannelPolicies.Converters; + +public static class DeliveryChannelPolicyConverter +{ + public static DLCS.HydraModel.DeliveryChannelPolicy ToHydra( + this DLCS.Model.Policies.DeliveryChannelPolicy deliveryChannelPolicy, + string baseUrl) + { + return new DLCS.HydraModel.DeliveryChannelPolicy(baseUrl) + { + Name = deliveryChannelPolicy.Name, + DisplayName = deliveryChannelPolicy.DisplayName, + Channel = deliveryChannelPolicy.Channel, + PolicyData = deliveryChannelPolicy.PolicyData, + PolicyModified = deliveryChannelPolicy.Modified, + }; + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 8df8a98ab..afbcb40ea 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -1,4 +1,6 @@  +using API.Features.DeliveryChannelPolicies.Converters; +using API.Features.DeliveryChannelPolicies.Requests; using API.Features.DeliveryChannelPolicies.Validation; using API.Infrastructure; using API.Settings; @@ -21,24 +23,30 @@ public class DeliveryChannelPoliciesController : HydraController IMediator mediator, IOptions options) : base(options.Value, mediator) { + } - + [HttpGet] - public async Task GetPolicyCollections(int customerId) + public async Task GetDeliveryChannelPolicyCollections( + [FromRoute] int customerId, + CancellationToken cancellationToken) { throw new NotImplementedException(); } - [HttpGet("{channelId}")] - public async Task GetPolicyCollection(int customerId, string channelId) + [HttpGet("{channelName}")] + public async Task GetDeliveryChannelPolicyCollection( + [FromRoute] int customerId, + [FromRoute] string channelId, + CancellationToken cancellationToken) { throw new NotImplementedException(); } - [HttpPost("{channelId}")] - public async Task PostPolicy( + [HttpPost("{channelName}")] + public async Task PostDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelId, + [FromRoute] string channelName, [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) @@ -53,21 +61,28 @@ public async Task GetPolicyCollection(int customerId, string chan throw new NotImplementedException(); } - [HttpGet("{channelId}/{policyId}")] - [ProducesResponseType(200, Type = typeof(DLCS.HydraModel.DeliveryChannelPolicy))] - public async Task GetPolicy( + [HttpGet("{channelName}/{policyName}")] + public async Task GetDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelId, - [FromRoute] string policyId) + [FromRoute] string channelName, + [FromRoute] string policyName, + CancellationToken cancellationToken) { - throw new NotImplementedException(); + var getDeliveryChannelPolicy = new GetDeliveryChannelPolicy(customerId, channelName, policyName ); + + return await HandleFetch( + getDeliveryChannelPolicy, + policy => policy.ToHydra(GetUrlRoots().BaseUrl), + errorTitle: "Get delivery channel policy failed", + cancellationToken: cancellationToken + ); } - [HttpPost("{channelId}/{policyId}")] - public async Task PutPolicy( + [HttpPost("{channelName}/{policyName}")] + public async Task PutDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelId, - [FromRoute] string policyId, + [FromRoute] string channelName, + [FromRoute] string policyName, [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) @@ -82,11 +97,11 @@ public async Task GetPolicyCollection(int customerId, string chan throw new NotImplementedException(); } - [HttpPatch("{channelId}/{policyId}")] - public async Task PatchPolicy( + [HttpPatch("{channelId}/{policyName}")] + public async Task PatchDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelId, - [FromRoute] string policyId, + [FromRoute] string channelName, + [FromRoute] string policyName, [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) @@ -101,8 +116,12 @@ public async Task GetPolicyCollection(int customerId, string chan throw new NotImplementedException(); } - [HttpDelete("{channelId}/{policyId}")] - public async Task DeletePolicy(int customerId, string channelId, string policyId) + [HttpDelete("{channelId}/{policyName}")] + public async Task DeleteDeliveryChannelPolicy( + [FromRoute] int customerId, + [FromRoute] string channelName, + [FromRoute] string policyName, + CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs new file mode 100644 index 000000000..6f5bea185 --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs @@ -0,0 +1,46 @@ +using API.Infrastructure.Requests; +using DLCS.Model.Policies; +using DLCS.Repository; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace API.Features.DeliveryChannelPolicies.Requests; + +public class GetDeliveryChannelPolicy: IRequest> +{ + public int CustomerId { get; } + public string ChannelName { get; set; } + public string PolicyName { get; set; } + + public GetDeliveryChannelPolicy(int customerId, string channelName, string policyName) + { + CustomerId = customerId; + ChannelName = channelName; + PolicyName = policyName; + } +} + +public class GetDeliveryChannelPolicyHandler : IRequestHandler> +{ + private readonly DlcsContext dbContext; + + public GetDeliveryChannelPolicyHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task> Handle(GetDeliveryChannelPolicy request, CancellationToken cancellationToken) + { + var deliveryChannelPolicy = await dbContext.DeliveryChannelPolicies + .AsNoTracking() + .SingleOrDefaultAsync(p => + p.Customer == request.CustomerId && + p.Channel == request.ChannelName && + p.Name == request.PolicyName, + cancellationToken); + + return deliveryChannelPolicy == null + ? FetchEntityResult.NotFound() + : FetchEntityResult.Success(deliveryChannelPolicy); + } +} \ No newline at end of file From 68c0b64192ad71460f1727f55ef9129900c73d3e Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 14 Feb 2024 14:40:22 +0000 Subject: [PATCH 03/35] Implement DELETE and GET Delivery Channel Policy --- .../DeliveryChannelPoliciesController.cs | 45 ++++++++----- .../Requests/DeleteDeliveryChannelPolicy.cs | 65 +++++++++++++++++++ .../Requests/GetDeliveryChannelPolicy.cs | 14 ++-- .../HydraDeliveryChannelPolicyValidator.cs | 2 +- 4 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index afbcb40ea..0e7272c9c 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -34,7 +34,8 @@ public class DeliveryChannelPoliciesController : HydraController throw new NotImplementedException(); } - [HttpGet("{channelName}")] + [HttpGet] + [Route("{channelName}")] public async Task GetDeliveryChannelPolicyCollection( [FromRoute] int customerId, [FromRoute] string channelId, @@ -43,10 +44,11 @@ public class DeliveryChannelPoliciesController : HydraController throw new NotImplementedException(); } - [HttpPost("{channelName}")] + [HttpPost] + [Route("{channelName}")] public async Task PostDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelName, + [FromRoute] string deliveryChannelName, [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) @@ -61,14 +63,16 @@ public class DeliveryChannelPoliciesController : HydraController throw new NotImplementedException(); } - [HttpGet("{channelName}/{policyName}")] + [HttpGet] + [Route("{channelName}/{policyName}")] public async Task GetDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelName, - [FromRoute] string policyName, + [FromRoute] string deliveryChannelName, + [FromRoute] string deliveryChannelPolicyName, CancellationToken cancellationToken) { - var getDeliveryChannelPolicy = new GetDeliveryChannelPolicy(customerId, channelName, policyName ); + var getDeliveryChannelPolicy = + new GetDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName); return await HandleFetch( getDeliveryChannelPolicy, @@ -78,11 +82,12 @@ public class DeliveryChannelPoliciesController : HydraController ); } - [HttpPost("{channelName}/{policyName}")] + [HttpPost] + [Route("{channelName}/{policyName}")] public async Task PutDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelName, - [FromRoute] string policyName, + [FromRoute] string deliveryChannelName, + [FromRoute] string deliveryChannelPolicyName, [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) @@ -97,11 +102,12 @@ public class DeliveryChannelPoliciesController : HydraController throw new NotImplementedException(); } - [HttpPatch("{channelId}/{policyName}")] + [HttpPatch] + [Route("{channelId}/{policyName}")] public async Task PatchDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelName, - [FromRoute] string policyName, + [FromRoute] string deliveryChannelName, + [FromRoute] string deliveryChannelPolicyName, [FromBody] DeliveryChannelPolicy hydraDeliveryChannelPolicy, [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) @@ -116,13 +122,16 @@ public class DeliveryChannelPoliciesController : HydraController throw new NotImplementedException(); } - [HttpDelete("{channelId}/{policyName}")] + [HttpDelete] + [Route("{channelId}/{policyName}")] public async Task DeleteDeliveryChannelPolicy( [FromRoute] int customerId, - [FromRoute] string channelName, - [FromRoute] string policyName, - CancellationToken cancellationToken) + [FromRoute] string deliveryChannelName, + [FromRoute] string deliveryChannelPolicyName) { - throw new NotImplementedException(); + var deleteDeliveryChannelPolicy = + new DeleteDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName); + + return await HandleDelete(deleteDeliveryChannelPolicy); } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs new file mode 100644 index 000000000..16145246f --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs @@ -0,0 +1,65 @@ +using API.Infrastructure.Requests; +using DLCS.Core; +using DLCS.Repository; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace API.Features.DeliveryChannelPolicies.Requests; + +public class DeleteDeliveryChannelPolicy: IRequest> +{ + public int CustomerId { get; } + public string DeliveryChannelName { get; set; } + public string DeliveryChannelPolicyName { get; set; } + + public DeleteDeliveryChannelPolicy(int customerId, string deliveryChannelName, string deliveryChannelPolicyName) + { + CustomerId = customerId; + DeliveryChannelName = deliveryChannelName; + DeliveryChannelPolicyName = deliveryChannelPolicyName; + } +} + +public class DeleteDeliveryChannelPolicyHandler : IRequestHandler> +{ + private readonly DlcsContext dbContext; + + public DeleteDeliveryChannelPolicyHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task> Handle(DeleteDeliveryChannelPolicy request, CancellationToken cancellationToken) + { + var policy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => + p.Name == request.DeliveryChannelPolicyName && + p.Channel == request.DeliveryChannelName, + cancellationToken); + + if (policy == null) + { + return new ResultMessage( + $"Deletion failed - Delivery channel policy ${request.DeliveryChannelPolicyName} was not found", DeleteResult.NotFound); + } + + var policyInUseByDefaultDeliveryChannel = await dbContext.DefaultDeliveryChannels.AnyAsync(c => + c.DeliveryChannelPolicyId == policy.Id, + cancellationToken); + + var policyInUseByAsset = await dbContext.ImageDeliveryChannels.AnyAsync(c => + c.DeliveryChannelPolicyId == policy.Id, + cancellationToken); + + if (policyInUseByDefaultDeliveryChannel || policyInUseByAsset) + { + return new ResultMessage( + $"Deletion failed - Delivery channel policy {request.DeliveryChannelPolicyName} is still in use", DeleteResult.Conflict); + } + + dbContext.DeliveryChannelPolicies.Remove(policy); + await dbContext.SaveChangesAsync(cancellationToken); + + return new ResultMessage( + $"Delivery channel policy {request.DeliveryChannelPolicyName} successfully deleted", DeleteResult.Deleted); + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs index 6f5bea185..03e54926d 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs @@ -9,14 +9,14 @@ namespace API.Features.DeliveryChannelPolicies.Requests; public class GetDeliveryChannelPolicy: IRequest> { public int CustomerId { get; } - public string ChannelName { get; set; } - public string PolicyName { get; set; } + public string DeliveryChannelName { get; set; } + public string DeliveryChannelPolicyName { get; set; } - public GetDeliveryChannelPolicy(int customerId, string channelName, string policyName) + public GetDeliveryChannelPolicy(int customerId, string deliveryChannelName, string deliveryChannelPolicyName) { CustomerId = customerId; - ChannelName = channelName; - PolicyName = policyName; + DeliveryChannelName = deliveryChannelName; + DeliveryChannelPolicyName = deliveryChannelPolicyName; } } @@ -35,8 +35,8 @@ public async Task> Handle(GetDeliveryCh .AsNoTracking() .SingleOrDefaultAsync(p => p.Customer == request.CustomerId && - p.Channel == request.ChannelName && - p.Name == request.PolicyName, + p.Channel == request.DeliveryChannelName && + p.Name == request.DeliveryChannelPolicyName, cancellationToken); return deliveryChannelPolicy == null diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs index f229c1a15..621647b4f 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -32,7 +32,7 @@ public HydraDeliveryChannelPolicyValidator() .Empty().WithMessage("'Channel' cannot be modified in a PATCH operation"); }); RuleFor(p => p.Channel) - .Must(c => allowedDeliveryChannels.Contains(c)) + .Must(c => c == null || allowedDeliveryChannels.Contains(c)) .WithMessage(p => $"'{p.Channel}' is not a supported delivery channel"); RuleFor(p => p.PolicyModified) .Empty().WithMessage(c => $"'policyModified' is not permitted"); From e35d44754b23c6bf9cf5b6ab7937527cce940030 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 14 Feb 2024 15:59:29 +0000 Subject: [PATCH 04/35] Implement CreateDeliveryChannelPolicy request, add Created property to DeliveryChannelPolicy hydra model, add ToDlcsModel() method to DeliveryChannelPolicyConverter --- .../DeliveryChannelPolicyConverter.cs | 17 ++++- .../DeliveryChannelPoliciesController.cs | 26 +++++--- .../Requests/CreateDeliveryChannelPolicy.cs | 66 +++++++++++++++++++ .../HydraDeliveryChannelPolicyValidator.cs | 4 +- .../DLCS.HydraModel/DeliveryChannelPolicy.cs | 11 +++- 5 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs index 1939d263b..aff055d09 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs @@ -12,7 +12,22 @@ public static class DeliveryChannelPolicyConverter DisplayName = deliveryChannelPolicy.DisplayName, Channel = deliveryChannelPolicy.Channel, PolicyData = deliveryChannelPolicy.PolicyData, - PolicyModified = deliveryChannelPolicy.Modified, + Created = deliveryChannelPolicy.Created, + Modified = deliveryChannelPolicy.Modified, + }; + } + + public static DLCS.Model.Policies.DeliveryChannelPolicy ToDlcsModel( + this DLCS.HydraModel.DeliveryChannelPolicy hydraDeliveryChannelPolicy) + { + return new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Name = hydraDeliveryChannelPolicy.Name, + DisplayName = hydraDeliveryChannelPolicy.DisplayName, + Channel = hydraDeliveryChannelPolicy.Channel, + PolicyData = hydraDeliveryChannelPolicy.PolicyData, + Created = hydraDeliveryChannelPolicy.Created.Value, // todo: deal with the nullable values here + Modified = hydraDeliveryChannelPolicy.Modified.Value }; } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 0e7272c9c..5c3d852a0 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -35,17 +35,17 @@ public class DeliveryChannelPoliciesController : HydraController } [HttpGet] - [Route("{channelName}")] + [Route("{deliveryChannelName}")] public async Task GetDeliveryChannelPolicyCollection( [FromRoute] int customerId, - [FromRoute] string channelId, + [FromRoute] string deliveryChannelName, CancellationToken cancellationToken) { throw new NotImplementedException(); } [HttpPost] - [Route("{channelName}")] + [Route("{deliveryChannelName}")] public async Task PostDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -53,6 +53,8 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; // Channel + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "post"), cancellationToken); if (!validationResult.IsValid) @@ -60,11 +62,17 @@ public class DeliveryChannelPoliciesController : HydraController return this.ValidationFailed(validationResult); } - throw new NotImplementedException(); + hydraDeliveryChannelPolicy.CustomerId = customerId; + var request = new CreateDeliveryChannelPolicy(customerId, hydraDeliveryChannelPolicy.ToDlcsModel()); + + return await HandleUpsert(request, + s => s.ToHydra(GetUrlRoots().BaseUrl), + errorTitle: "Failed to create delivery channel policy", + cancellationToken: cancellationToken); } [HttpGet] - [Route("{channelName}/{policyName}")] + [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] public async Task GetDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -83,7 +91,7 @@ public class DeliveryChannelPoliciesController : HydraController } [HttpPost] - [Route("{channelName}/{policyName}")] + [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] public async Task PutDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -92,6 +100,8 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; + hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "put"), cancellationToken); if (!validationResult.IsValid) @@ -103,7 +113,7 @@ public class DeliveryChannelPoliciesController : HydraController } [HttpPatch] - [Route("{channelId}/{policyName}")] + [Route("{deliveryChannelId}/{deliveryChannelPolicyName}")] public async Task PatchDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -123,7 +133,7 @@ public class DeliveryChannelPoliciesController : HydraController } [HttpDelete] - [Route("{channelId}/{policyName}")] + [Route("{deliveryChannelId}/{deliveryChannelPolicyName}")] public async Task DeleteDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs new file mode 100644 index 000000000..a6ba07217 --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs @@ -0,0 +1,66 @@ +using API.Infrastructure.Requests; +using DLCS.Core; +using DLCS.Model.Policies; +using DLCS.Repository; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace API.Features.DeliveryChannelPolicies.Requests; + +public class CreateDeliveryChannelPolicy : IRequest> +{ + public int CustomerId { get; } + + public DeliveryChannelPolicy DeliveryChannelPolicy { get; } + + public CreateDeliveryChannelPolicy(int customerId, DeliveryChannelPolicy deliveryChannelPolicy) + { + CustomerId = customerId; + DeliveryChannelPolicy = deliveryChannelPolicy; + } +} + +public class CreateDeliveryChannelPolicyHandler : IRequestHandler> +{ + private readonly DlcsContext dbContext; + + public CreateDeliveryChannelPolicyHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task> Handle(CreateDeliveryChannelPolicy request, CancellationToken cancellationToken) + { + var nameInUse = await dbContext.DeliveryChannelPolicies.AnyAsync(p => + p.Customer == request.CustomerId && + p.Channel == request.DeliveryChannelPolicy.Channel && + p.Name == request.DeliveryChannelPolicy.Name, + cancellationToken); + + if (nameInUse) + { + return ModifyEntityResult.Failure( + $"A policy for delivery channel '{request.DeliveryChannelPolicy.Channel}' called '{request.DeliveryChannelPolicy.Name}' already exists" , + WriteResult.Conflict); + } + + var newDeliveryChannelPolicy = new DeliveryChannelPolicy() + { + Customer = request.DeliveryChannelPolicy.Customer, + Name = request.DeliveryChannelPolicy.Name, + DisplayName = request.DeliveryChannelPolicy.DisplayName, + Channel = request.DeliveryChannelPolicy.Channel, + System = false, + Modified = DateTime.Now, + Created = DateTime.Now, + PolicyData = request.DeliveryChannelPolicy.PolicyData, + }; + + await dbContext.DeliveryChannelPolicies.AddAsync(newDeliveryChannelPolicy, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return ModifyEntityResult.Success(newDeliveryChannelPolicy, WriteResult.Created); + } +} + + diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs index 621647b4f..9f80d6157 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -34,7 +34,9 @@ public HydraDeliveryChannelPolicyValidator() RuleFor(p => p.Channel) .Must(c => c == null || allowedDeliveryChannels.Contains(c)) .WithMessage(p => $"'{p.Channel}' is not a supported delivery channel"); - RuleFor(p => p.PolicyModified) + RuleFor(p => p.Modified) .Empty().WithMessage(c => $"'policyModified' is not permitted"); + RuleFor(p => p.Created) + .Empty().WithMessage(c => $"'policyCreated' is not permitted"); } } \ No newline at end of file diff --git a/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs b/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs index 1ab4d396e..2f38a3c63 100644 --- a/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs +++ b/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs @@ -10,9 +10,11 @@ namespace DLCS.HydraModel; UriTemplate = "/customers/{0}/deliveryChannelPolicies/{1}/{2}")] public class DeliveryChannelPolicy : DlcsResource { + [JsonIgnore] + public int CustomerId { get; set; } + public DeliveryChannelPolicy() { - } public DeliveryChannelPolicy(string baseUrl) @@ -40,10 +42,15 @@ public DeliveryChannelPolicy(string baseUrl) [JsonProperty(Order = 13, PropertyName = "policyData")] public string? PolicyData { get; set; } + [RdfProperty(Description = "The date this policy was created.", + Range = Names.XmlSchema.DateTime, ReadOnly = true, WriteOnly = false)] + [JsonProperty(Order = 14, PropertyName = "policyCreated")] + public DateTime? Created { get; set; } + [RdfProperty(Description = "The date this policy was last modified.", Range = Names.XmlSchema.DateTime, ReadOnly = true, WriteOnly = false)] [JsonProperty(Order = 14, PropertyName = "policyModified")] - public DateTime? PolicyModified { get; set; } + public DateTime? Modified { get; set; } } public class DeliveryChannelPolicyClass: Class From 71d1daa2dd46117be5ac211dfbae77686d2f7334 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 14 Feb 2024 16:37:51 +0000 Subject: [PATCH 05/35] Use DateTime.UtcNow for delivery channel policy timestamps --- .../Converters/DeliveryChannelPolicyConverter.cs | 10 ++++++++-- .../DeliveryChannelPoliciesController.cs | 2 +- .../Requests/CreateDeliveryChannelPolicy.cs | 4 ++-- .../HydraDeliveryChannelPolicyValidator.cs | 16 +++++++++++----- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs index aff055d09..ddcb3f01e 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs @@ -8,6 +8,7 @@ public static class DeliveryChannelPolicyConverter { return new DLCS.HydraModel.DeliveryChannelPolicy(baseUrl) { + CustomerId = deliveryChannelPolicy.Customer, Name = deliveryChannelPolicy.Name, DisplayName = deliveryChannelPolicy.DisplayName, Channel = deliveryChannelPolicy.Channel, @@ -22,12 +23,17 @@ public static class DeliveryChannelPolicyConverter { return new DLCS.Model.Policies.DeliveryChannelPolicy() { + Customer = hydraDeliveryChannelPolicy.CustomerId, Name = hydraDeliveryChannelPolicy.Name, DisplayName = hydraDeliveryChannelPolicy.DisplayName, Channel = hydraDeliveryChannelPolicy.Channel, PolicyData = hydraDeliveryChannelPolicy.PolicyData, - Created = hydraDeliveryChannelPolicy.Created.Value, // todo: deal with the nullable values here - Modified = hydraDeliveryChannelPolicy.Modified.Value + Created = hydraDeliveryChannelPolicy.Created.HasValue // find a better way to deal with these + ? hydraDeliveryChannelPolicy.Created.Value + : DateTime.MinValue, + Modified = hydraDeliveryChannelPolicy.Modified.HasValue + ? hydraDeliveryChannelPolicy.Modified.Value + : DateTime.MinValue, }; } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 5c3d852a0..50749f5dd 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -53,7 +53,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; // Channel + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; // Model channel should be from path var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "post"), cancellationToken); diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs index a6ba07217..9ed8ff681 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs @@ -51,8 +51,8 @@ public async Task> Handle(CreateDelive DisplayName = request.DeliveryChannelPolicy.DisplayName, Channel = request.DeliveryChannelPolicy.Channel, System = false, - Modified = DateTime.Now, - Created = DateTime.Now, + Modified = DateTime.UtcNow, + Created = DateTime.UtcNow, PolicyData = request.DeliveryChannelPolicy.PolicyData, }; diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs index 9f80d6157..fe5d77397 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -14,25 +14,31 @@ public HydraDeliveryChannelPolicyValidator() RuleFor(p => p.Id) .Empty() .WithMessage(p => $"DLCS must allocate named origin strategy id, but id {p.Id} was supplied"); + RuleFor(p => p.CustomerId) + .Empty() + .WithMessage("Should not include user id"); RuleSet("post", () => { + RuleFor(c => c.Name) + .NotEmpty().WithMessage("'name' is required"); RuleFor(c => c.Channel) - .Empty().WithMessage("'name' is required"); + .NotEmpty().WithMessage("'channel' is required"); }); RuleSet("put", () => { - RuleFor(c => c.Channel) + RuleFor(c => c.Name) .NotEmpty().WithMessage("'name' is not permitted"); }); RuleSet("patch", () => { RuleFor(c => c.Channel) - .Empty().WithMessage("'Name' cannot be modified in a PATCH operation"); + .Empty().WithMessage("'name' cannot be modified in a PATCH operation"); RuleFor(c => c.Channel) - .Empty().WithMessage("'Channel' cannot be modified in a PATCH operation"); + .Empty().WithMessage("'channel' cannot be modified in a PATCH operation"); }); RuleFor(p => p.Channel) - .Must(c => c == null || allowedDeliveryChannels.Contains(c)) + .Must(c => allowedDeliveryChannels.Contains(c)) + .When(p => p != null) .WithMessage(p => $"'{p.Channel}' is not a supported delivery channel"); RuleFor(p => p.Modified) .Empty().WithMessage(c => $"'policyModified' is not permitted"); From 34379e23afaae9833e71538002480e7f6b924853 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 14 Feb 2024 17:11:45 +0000 Subject: [PATCH 06/35] Add Get_DeliveryChannelPolicy_200() and Get_DeliveryChannelPolicy_404_IfNotFound() tests --- .../Integration/CustomerPolicyTests.cs | 33 +++++++++++++++++++ .../Integration/DlcsDatabaseFixture.cs | 10 ++++++ 2 files changed, 43 insertions(+) diff --git a/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs b/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs index 9a2fdc675..3e87ae995 100644 --- a/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs +++ b/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs @@ -93,4 +93,37 @@ public async Task Get_ImageOptimisationPolicy_404_IfNotFound() // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + + [Fact] + public async Task Get_DeliveryChannelPolicy_200() + { + // Arrange + var path = $"customers/99/deliveryChannelPolicies/thumbs/example-thumbs-policy"; + + // Act + var response = await httpClient.AsCustomer(99).GetAsync(path); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var model = await response.ReadAsHydraResponseAsync(); + model.Name.Should().Be("example-thumbs-policy"); + model.DisplayName.Should().Be("Example Thumbnail Policy"); + model.Channel.Should().Be("thumbs"); + model.PolicyData.Should().Be("{[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]}"); + } + + [Fact] + public async Task Get_DeliveryChannelPolicy_404_IfNotFound() + { + // Arrange + var path = $"customers/99/deliveryChannelPolicies/thumbs/foofoo"; + + // Act + var response = await httpClient.AsCustomer(99).GetAsync(path); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + } \ No newline at end of file diff --git a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs index 6c3cd68f7..7b728db10 100644 --- a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs +++ b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs @@ -50,6 +50,7 @@ public void CleanUp() DbContext.Database.ExecuteSqlRaw("DELETE FROM \"Customers\" WHERE \"Id\" != 99"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"StoragePolicies\" WHERE \"Id\" not in ('default', 'small', 'medium')"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"ThumbnailPolicies\" WHERE \"Id\" != 'default'"); + DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DeliveryChannelPolicies\" WHERE \"Customer\" != 99"); DbContext.Database.ExecuteSqlRaw( "DELETE FROM \"ImageOptimisationPolicies\" WHERE \"Id\" not in ('fast-higher', 'video-max', 'audio-max', 'cust-default')"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"Images\""); @@ -164,6 +165,15 @@ await DbContext.EntityCounters.AddAsync(new EntityCounter() Id = "cust-default", Name = "Customer Scoped", TechnicalDetails = new[] { "default" }, Global = false, Customer = 99 }); + await DbContext.DeliveryChannelPolicies.AddAsync(new DeliveryChannelPolicy() + { + Customer = 99, + Name = "example-thumbs-policy", + DisplayName = "Example Thumbnail Policy", + Channel = "thumbs", + PolicyData = "{[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]}", + System = false, + }); await DbContext.AuthServices.AddAsync(new AuthService { Customer = customer, Name = "clickthrough", Id = ClickThroughAuthService, Description = "", Label = "", From 5d87794abda875c497689acfb15c73972ffa748e Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 15 Feb 2024 14:49:39 +0000 Subject: [PATCH 07/35] Add UpdateDeliveryChannel work in progress --- .../DeliveryChannelPoliciesController.cs | 17 ++++- .../Requests/CreateDeliveryChannelPolicy.cs | 5 +- .../Requests/UpdateDeliveryChannelPolicy.cs | 73 +++++++++++++++++++ .../HydraDeliveryChannelPolicyValidator.cs | 25 +++---- 4 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 50749f5dd..7d8d4cead 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -86,11 +86,10 @@ public class DeliveryChannelPoliciesController : HydraController getDeliveryChannelPolicy, policy => policy.ToHydra(GetUrlRoots().BaseUrl), errorTitle: "Get delivery channel policy failed", - cancellationToken: cancellationToken - ); + cancellationToken: cancellationToken); } - [HttpPost] + [HttpPut] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] public async Task PutDeliveryChannelPolicy( [FromRoute] int customerId, @@ -102,6 +101,7 @@ public class DeliveryChannelPoliciesController : HydraController { hydraDeliveryChannelPolicy.Channel = deliveryChannelName; hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "put"), cancellationToken); if (!validationResult.IsValid) @@ -109,7 +109,13 @@ public class DeliveryChannelPoliciesController : HydraController return this.ValidationFailed(validationResult); } - throw new NotImplementedException(); + var updateDeliveryChannelPolicy = + new UpdateDeliveryChannelPolicy(customerId, hydraDeliveryChannelPolicy.ToDlcsModel()); + + return await HandleUpsert(updateDeliveryChannelPolicy, + s => s.ToHydra(GetUrlRoots().BaseUrl), + errorTitle: "Failed to update delivery channel policy", + cancellationToken: cancellationToken); } [HttpPatch] @@ -122,6 +128,9 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; + hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "patch"), cancellationToken); if (!validationResult.IsValid) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs index 9ed8ff681..59b24b7ba 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs @@ -44,12 +44,13 @@ public async Task> Handle(CreateDelive WriteResult.Conflict); } + // todo: validate channel + policyData var newDeliveryChannelPolicy = new DeliveryChannelPolicy() { - Customer = request.DeliveryChannelPolicy.Customer, + Customer = request.CustomerId, Name = request.DeliveryChannelPolicy.Name, DisplayName = request.DeliveryChannelPolicy.DisplayName, - Channel = request.DeliveryChannelPolicy.Channel, + Channel = request.DeliveryChannelPolicy.Channel, System = false, Modified = DateTime.UtcNow, Created = DateTime.UtcNow, diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs new file mode 100644 index 000000000..8d11b2ac2 --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs @@ -0,0 +1,73 @@ +using API.Infrastructure.Requests; +using DLCS.Core; +using DLCS.Model.Policies; +using DLCS.Repository; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace API.Features.DeliveryChannelPolicies.Requests; + +public class UpdateDeliveryChannelPolicy : IRequest> +{ + public int CustomerId { get; } + + public DeliveryChannelPolicy DeliveryChannelPolicy { get; } + + public UpdateDeliveryChannelPolicy(int customerId, DeliveryChannelPolicy deliveryChannelPolicy) + { + CustomerId = customerId; + DeliveryChannelPolicy = deliveryChannelPolicy; + } +} + +public class UpdateDeliveryChannelPolicyHandler : IRequestHandler> +{ + private readonly DlcsContext dbContext; + + public UpdateDeliveryChannelPolicyHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task> Handle(UpdateDeliveryChannelPolicy request, CancellationToken cancellationToken) + { + var existingDeliveryChannelPolicy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => + p.Customer == request.CustomerId && + p.Channel == request.DeliveryChannelPolicy.Channel && + p.Name == request.DeliveryChannelPolicy.Name, + cancellationToken); + + if (existingDeliveryChannelPolicy == null) + { + // todo: validate channel + policydata + var newDeliveryChannelPolicy = new DeliveryChannelPolicy() + { + Customer = request.CustomerId, + Name = request.DeliveryChannelPolicy.Name, + DisplayName = request.DeliveryChannelPolicy.DisplayName, + Channel = request.DeliveryChannelPolicy.Channel, + System = false, + Modified = DateTime.UtcNow, + Created = DateTime.UtcNow, + PolicyData = request.DeliveryChannelPolicy.PolicyData, + }; + + await dbContext.DeliveryChannelPolicies.AddAsync(newDeliveryChannelPolicy, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return ModifyEntityResult.Success(newDeliveryChannelPolicy, WriteResult.Created); + } + + existingDeliveryChannelPolicy.DisplayName = request.DeliveryChannelPolicy.DisplayName; + + // todo: validate channel + policyData + existingDeliveryChannelPolicy.Channel = request.DeliveryChannelPolicy.Channel; + existingDeliveryChannelPolicy.PolicyData = request.DeliveryChannelPolicy.PolicyData; + + existingDeliveryChannelPolicy.Modified = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(cancellationToken); + + return ModifyEntityResult.Success(existingDeliveryChannelPolicy); + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs index fe5d77397..7de7b6a8b 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -8,41 +8,34 @@ namespace API.Features.DeliveryChannelPolicies.Validation; public class HydraDeliveryChannelPolicyValidator : AbstractValidator { private readonly string[] allowedDeliveryChannels = {"iiif-img", "iiif-av", "thumbs"}; - + public HydraDeliveryChannelPolicyValidator() { RuleFor(p => p.Id) .Empty() - .WithMessage(p => $"DLCS must allocate named origin strategy id, but id {p.Id} was supplied"); + .WithMessage(p => $"DLCS must allocate named delivery channel policy id, but id {p.Id} was supplied"); RuleFor(p => p.CustomerId) .Empty() .WithMessage("Should not include user id"); RuleSet("post", () => { - RuleFor(c => c.Name) - .NotEmpty().WithMessage("'name' is required"); - RuleFor(c => c.Channel) - .NotEmpty().WithMessage("'channel' is required"); + }); RuleSet("put", () => { - RuleFor(c => c.Name) - .NotEmpty().WithMessage("'name' is not permitted"); - }); + + }); RuleSet("patch", () => { - RuleFor(c => c.Channel) - .Empty().WithMessage("'name' cannot be modified in a PATCH operation"); - RuleFor(c => c.Channel) - .Empty().WithMessage("'channel' cannot be modified in a PATCH operation"); - }); + + }); RuleFor(p => p.Channel) .Must(c => allowedDeliveryChannels.Contains(c)) .When(p => p != null) .WithMessage(p => $"'{p.Channel}' is not a supported delivery channel"); RuleFor(p => p.Modified) - .Empty().WithMessage(c => $"'policyModified' is not permitted"); + .Empty().WithMessage(c => $"'policyModified' is generated by the DLCS and cannot be specified"); RuleFor(p => p.Created) - .Empty().WithMessage(c => $"'policyCreated' is not permitted"); + .Empty().WithMessage(c => $"'policyCreated' is generated by the DLCS and cannot be specified"); } } \ No newline at end of file From fc3cfcc2fad221bcfe640f07e7fa37350bef3e8d Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 09:00:48 +0000 Subject: [PATCH 08/35] Implement PATCH route for DeliveryChannelPolicyController --- .../DeliveryChannelPoliciesController.cs | 66 ++++++++++++--- .../Requests/CreateDeliveryChannelPolicy.cs | 3 +- .../Requests/PatchDeliveryChannelPolicy.cs | 82 +++++++++++++++++++ .../Requests/UpdateDeliveryChannelPolicy.cs | 27 +++--- .../HydraDeliveryChannelPolicyValidator.cs | 25 +++--- 5 files changed, 159 insertions(+), 44 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 7d8d4cead..8f4147327 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -1,12 +1,15 @@  +using System.Collections.Generic; using API.Features.DeliveryChannelPolicies.Converters; using API.Features.DeliveryChannelPolicies.Requests; using API.Features.DeliveryChannelPolicies.Validation; using API.Infrastructure; using API.Settings; using DLCS.HydraModel; +using DLCS.Model.Assets; using FluentValidation; using MediatR; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -19,6 +22,8 @@ namespace API.Features.DeliveryChannelPolicies; [ApiController] public class DeliveryChannelPoliciesController : HydraController { + private readonly string[] allowedDeliveryChannels = {"iiif-img", "iiif-av", "thumbs"}; + public DeliveryChannelPoliciesController( IMediator mediator, IOptions options) : base(options.Value, mediator) @@ -27,14 +32,16 @@ public class DeliveryChannelPoliciesController : HydraController } [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetDeliveryChannelPolicyCollections( [FromRoute] int customerId, CancellationToken cancellationToken) { - throw new NotImplementedException(); + throw new NotImplementedException(); } [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] [Route("{deliveryChannelName}")] public async Task GetDeliveryChannelPolicyCollection( [FromRoute] int customerId, @@ -53,7 +60,11 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; // Model channel should be from path + if(!IsValidDeliveryChannelPolicy(deliveryChannelName)) + { + return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + 400, "Invalid delivery channel policy"); + } var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "post"), cancellationToken); @@ -63,6 +74,7 @@ public class DeliveryChannelPoliciesController : HydraController } hydraDeliveryChannelPolicy.CustomerId = customerId; + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; var request = new CreateDeliveryChannelPolicy(customerId, hydraDeliveryChannelPolicy.ToDlcsModel()); return await HandleUpsert(request, @@ -99,19 +111,26 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; - hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + if(!IsValidDeliveryChannelPolicy(deliveryChannelName)) + { + return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + 400, "Invalid delivery channel policy"); + } var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, - policy => policy.IncludeRuleSets("default", "put"), cancellationToken); + policy => policy.IncludeRuleSets("default", "put-patch"), cancellationToken); if (!validationResult.IsValid) { return this.ValidationFailed(validationResult); } + + hydraDeliveryChannelPolicy.CustomerId = customerId; + hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; var updateDeliveryChannelPolicy = new UpdateDeliveryChannelPolicy(customerId, hydraDeliveryChannelPolicy.ToDlcsModel()); - + return await HandleUpsert(updateDeliveryChannelPolicy, s => s.ToHydra(GetUrlRoots().BaseUrl), errorTitle: "Failed to update delivery channel policy", @@ -119,7 +138,7 @@ public class DeliveryChannelPoliciesController : HydraController } [HttpPatch] - [Route("{deliveryChannelId}/{deliveryChannelPolicyName}")] + [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] public async Task PatchDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -128,21 +147,37 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; - hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + if(!IsValidDeliveryChannelPolicy(deliveryChannelName)) + { + return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + 400, "Invalid delivery channel policy"); + } var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, - policy => policy.IncludeRuleSets("default", "patch"), cancellationToken); + policy => policy.IncludeRuleSets("default", "put-patch"), cancellationToken); if (!validationResult.IsValid) { return this.ValidationFailed(validationResult); } - throw new NotImplementedException(); + hydraDeliveryChannelPolicy.CustomerId = customerId; + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; + hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + + var patchDeliveryChannelPolicy = new PatchDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName) + { + DisplayName = hydraDeliveryChannelPolicy.DisplayName, + PolicyData = hydraDeliveryChannelPolicy.PolicyData + }; + + return await HandleUpsert(patchDeliveryChannelPolicy, + s => s.ToHydra(GetUrlRoots().BaseUrl), + errorTitle: "Failed to update delivery channel policy", + cancellationToken: cancellationToken); } [HttpDelete] - [Route("{deliveryChannelId}/{deliveryChannelPolicyName}")] + [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] public async Task DeleteDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -152,5 +187,10 @@ public class DeliveryChannelPoliciesController : HydraController new DeleteDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName); return await HandleDelete(deleteDeliveryChannelPolicy); - } + } + + private bool IsValidDeliveryChannelPolicy(string deliveryChannelPolicyName) + { + return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); + } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs index 59b24b7ba..1e1e737e0 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs @@ -40,11 +40,10 @@ public async Task> Handle(CreateDelive if (nameInUse) { return ModifyEntityResult.Failure( - $"A policy for delivery channel '{request.DeliveryChannelPolicy.Channel}' called '{request.DeliveryChannelPolicy.Name}' already exists" , + $"A {request.DeliveryChannelPolicy.Channel}' policy called '{request.DeliveryChannelPolicy.Name}' already exists" , WriteResult.Conflict); } - // todo: validate channel + policyData var newDeliveryChannelPolicy = new DeliveryChannelPolicy() { Customer = request.CustomerId, diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs new file mode 100644 index 000000000..8a2250325 --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs @@ -0,0 +1,82 @@ +using API.Infrastructure.Requests; +using DLCS.Core; +using DLCS.Core.Strings; +using DLCS.Model.Policies; +using DLCS.Repository; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace API.Features.DeliveryChannelPolicies.Requests; + +public class PatchDeliveryChannelPolicy : IRequest> +{ + public int CustomerId { get; } + + public string Channel { get; set; } + + public string Name { get; set; } + + public string? DisplayName { get; set; } + + public string? PolicyData { get; set; } + + public PatchDeliveryChannelPolicy(int customerId, string channel, string name) + { + CustomerId = customerId; + Channel = channel; + Name = name; + } +} + +public class PatchDeliveryChannelPolicyHandler : IRequestHandler> +{ + private readonly DlcsContext dbContext; + + public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task> Handle(PatchDeliveryChannelPolicy request, + CancellationToken cancellationToken) + { + var existingDeliveryChannelPolicy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => + p.Customer == request.CustomerId && + p.Channel == request.Channel && + p.Name == request.Name, + cancellationToken); + + if (existingDeliveryChannelPolicy == null) + { + return ModifyEntityResult.Failure( + $"A policy for delivery channel '{request.Channel}' called '{request.Name}' was not found" , + WriteResult.NotFound); + } + + var hasBeenChanged = false; + + if (request.DisplayName.HasText()) + { + existingDeliveryChannelPolicy.DisplayName = request.DisplayName; + hasBeenChanged = true; + } + + if (request.PolicyData.HasText()) { + existingDeliveryChannelPolicy.PolicyData = request.PolicyData; + hasBeenChanged = true; + } + + if (hasBeenChanged) + { + existingDeliveryChannelPolicy.Modified = DateTime.UtcNow; + } + + var rowCount = await dbContext.SaveChangesAsync(cancellationToken); + if (rowCount == 0) + { + return ModifyEntityResult.Failure("Unable to patch delivery channel policy", WriteResult.Error); + } + + return ModifyEntityResult.Success(existingDeliveryChannelPolicy); + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs index 8d11b2ac2..ed5f01a62 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs @@ -36,10 +36,19 @@ public async Task> Handle(UpdateDelive p.Channel == request.DeliveryChannelPolicy.Channel && p.Name == request.DeliveryChannelPolicy.Name, cancellationToken); - - if (existingDeliveryChannelPolicy == null) + + if (existingDeliveryChannelPolicy != null) + { + existingDeliveryChannelPolicy.DisplayName = request.DeliveryChannelPolicy.DisplayName; + existingDeliveryChannelPolicy.Modified = DateTime.UtcNow; + existingDeliveryChannelPolicy.PolicyData = request.DeliveryChannelPolicy.PolicyData; + + await dbContext.SaveChangesAsync(cancellationToken); + + return ModifyEntityResult.Success(existingDeliveryChannelPolicy); + } + else { - // todo: validate channel + policydata var newDeliveryChannelPolicy = new DeliveryChannelPolicy() { Customer = request.CustomerId, @@ -57,17 +66,5 @@ public async Task> Handle(UpdateDelive return ModifyEntityResult.Success(newDeliveryChannelPolicy, WriteResult.Created); } - - existingDeliveryChannelPolicy.DisplayName = request.DeliveryChannelPolicy.DisplayName; - - // todo: validate channel + policyData - existingDeliveryChannelPolicy.Channel = request.DeliveryChannelPolicy.Channel; - existingDeliveryChannelPolicy.PolicyData = request.DeliveryChannelPolicy.PolicyData; - - existingDeliveryChannelPolicy.Modified = DateTime.UtcNow; - - await dbContext.SaveChangesAsync(cancellationToken); - - return ModifyEntityResult.Success(existingDeliveryChannelPolicy); } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs index 7de7b6a8b..a31efe7f7 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -7,8 +7,6 @@ namespace API.Features.DeliveryChannelPolicies.Validation; /// public class HydraDeliveryChannelPolicyValidator : AbstractValidator { - private readonly string[] allowedDeliveryChannels = {"iiif-img", "iiif-av", "thumbs"}; - public HydraDeliveryChannelPolicyValidator() { RuleFor(p => p.Id) @@ -19,23 +17,22 @@ public HydraDeliveryChannelPolicyValidator() .WithMessage("Should not include user id"); RuleSet("post", () => { - + RuleFor(p => p.Name) + .NotEmpty() + .WithMessage("'name' is required"); }); - RuleSet("put", () => + RuleSet("put-patch", () => { - + RuleFor(p => p.Name) + .Empty() + .WithMessage("'name' should be set in the URL"); }); - RuleSet("patch", () => - { - - }); RuleFor(p => p.Channel) - .Must(c => allowedDeliveryChannels.Contains(c)) - .When(p => p != null) - .WithMessage(p => $"'{p.Channel}' is not a supported delivery channel"); + .Empty() + .WithMessage("'channel' should be set in the URL"); RuleFor(p => p.Modified) - .Empty().WithMessage(c => $"'policyModified' is generated by the DLCS and cannot be specified"); + .Empty().WithMessage(c => $"'policyModified' is generated by the DLCS and cannot be set manually"); RuleFor(p => p.Created) - .Empty().WithMessage(c => $"'policyCreated' is generated by the DLCS and cannot be specified"); + .Empty().WithMessage(c => $"'policyCreated' is generated by the DLCS and cannot be set manually"); } } \ No newline at end of file From f9cd0692be782ff3ce392e36fd72ea5de4b33e5c Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 13:45:55 +0000 Subject: [PATCH 09/35] Include Thumbnails channel in AssetDeliveryChannels --- src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs b/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs index 07d331584..ba4639415 100644 --- a/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs +++ b/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs @@ -6,14 +6,16 @@ namespace DLCS.Model.Assets; public static class AssetDeliveryChannels { + public const string Image = "iiif-img"; + public const string Thumbnails = "thumbs"; public const string Timebased = "iiif-av"; public const string File = "file"; /// /// All possible delivery channels /// - public static string[] All { get; } = { File, Timebased, Image }; + public static string[] All { get; } = { File, Timebased, Image, Thumbnails }; /// /// All possible delivery channels as a comma-delimited string From 026991fba39604731b27f81bdb75e86ff7508c6c Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 14:40:26 +0000 Subject: [PATCH 10/35] Validate policyData in PATCH, PUT and POST --- .../DeliveryChannelPoliciesController.cs | 11 ++- .../Requests/CreateDeliveryChannelPolicy.cs | 10 ++- .../Requests/PatchDeliveryChannelPolicy.cs | 20 +++-- .../Requests/UpdateDeliveryChannelPolicy.cs | 10 ++- .../HydraDeliveryChannelPolicyValidator.cs | 6 ++ .../Validation/PolicyDataValidator.cs | 76 +++++++++++++++++++ 6 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 8f4147327..7cb4baf94 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -1,6 +1,4 @@ - -using System.Collections.Generic; -using API.Features.DeliveryChannelPolicies.Converters; +using API.Features.DeliveryChannelPolicies.Converters; using API.Features.DeliveryChannelPolicies.Requests; using API.Features.DeliveryChannelPolicies.Validation; using API.Infrastructure; @@ -22,7 +20,12 @@ namespace API.Features.DeliveryChannelPolicies; [ApiController] public class DeliveryChannelPoliciesController : HydraController { - private readonly string[] allowedDeliveryChannels = {"iiif-img", "iiif-av", "thumbs"}; + private readonly string[] allowedDeliveryChannels = + { + AssetDeliveryChannels.Image, + AssetDeliveryChannels.Timebased, + AssetDeliveryChannels.Thumbnails + }; public DeliveryChannelPoliciesController( IMediator mediator, diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs index 1e1e737e0..04cf6c507 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs @@ -1,4 +1,5 @@ -using API.Infrastructure.Requests; +using API.Features.DeliveryChannelPolicies.Validation; +using API.Infrastructure.Requests; using DLCS.Core; using DLCS.Model.Policies; using DLCS.Repository; @@ -43,6 +44,13 @@ public async Task> Handle(CreateDelive $"A {request.DeliveryChannelPolicy.Channel}' policy called '{request.DeliveryChannelPolicy.Name}' already exists" , WriteResult.Conflict); } + + if(!PolicyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) + { + return ModifyEntityResult.Failure( + $"'policyData' contains bad JSON or invalid data", + WriteResult.FailedValidation); + } var newDeliveryChannelPolicy = new DeliveryChannelPolicy() { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs index 8a2250325..f1b2c0605 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs @@ -1,4 +1,5 @@ -using API.Infrastructure.Requests; +using API.Features.DeliveryChannelPolicies.Validation; +using API.Infrastructure.Requests; using DLCS.Core; using DLCS.Core.Strings; using DLCS.Model.Policies; @@ -41,9 +42,9 @@ public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext) CancellationToken cancellationToken) { var existingDeliveryChannelPolicy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => - p.Customer == request.CustomerId && - p.Channel == request.Channel && - p.Name == request.Name, + p.Customer == request.CustomerId && + p.Channel == request.Channel && + p.Name == request.Name, cancellationToken); if (existingDeliveryChannelPolicy == null) @@ -61,8 +62,17 @@ public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext) hasBeenChanged = true; } - if (request.PolicyData.HasText()) { + if (request.PolicyData.HasText()) + { existingDeliveryChannelPolicy.PolicyData = request.PolicyData; + + if(!PolicyDataValidator.Validate(request.PolicyData, existingDeliveryChannelPolicy.Channel)) + { + return ModifyEntityResult.Failure( + $"'policyData' contains bad JSON or invalid data", + WriteResult.FailedValidation); + } + hasBeenChanged = true; } diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs index ed5f01a62..b03f09c0d 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs @@ -1,4 +1,5 @@ -using API.Infrastructure.Requests; +using API.Features.DeliveryChannelPolicies.Validation; +using API.Infrastructure.Requests; using DLCS.Core; using DLCS.Model.Policies; using DLCS.Repository; @@ -31,6 +32,13 @@ public UpdateDeliveryChannelPolicyHandler(DlcsContext dbContext) public async Task> Handle(UpdateDeliveryChannelPolicy request, CancellationToken cancellationToken) { + if(!PolicyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) + { + return ModifyEntityResult.Failure( + $"'policyData' contains bad JSON or invalid data", + WriteResult.FailedValidation); + } + var existingDeliveryChannelPolicy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => p.Customer == request.CustomerId && p.Channel == request.DeliveryChannelPolicy.Channel && diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs index a31efe7f7..8f8ca9c1e 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -20,12 +20,18 @@ public HydraDeliveryChannelPolicyValidator() RuleFor(p => p.Name) .NotEmpty() .WithMessage("'name' is required"); + RuleFor(p => p.PolicyData) + .NotEmpty() + .WithMessage("'policyData' is required"); }); RuleSet("put-patch", () => { RuleFor(p => p.Name) .Empty() .WithMessage("'name' should be set in the URL"); + RuleFor(p => p.PolicyData) + .NotEmpty() + .WithMessage("'policyData' is required"); }); RuleFor(p => p.Channel) .Empty() diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs new file mode 100644 index 000000000..8d572a7d9 --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using DLCS.Core.Collections; +using DLCS.Model.Assets; +using IIIF.ImageApi; + +namespace API.Features.DeliveryChannelPolicies.Validation; + +public static class PolicyDataValidator +{ + private static string[] validTimeBasedFormats = + { + "video-mp4-720p", + "audio-mp3-128" + }; + + public static bool Validate(string policyDataJson, string channel) + { + return channel switch + { + AssetDeliveryChannels.Thumbnails => ValidateThumbnailPolicyData(policyDataJson), + AssetDeliveryChannels.Timebased => ValidateTimeBasedPolicyData(policyDataJson), + _ => false // This is only for thumbs and iiif-av for now + }; + } + + private static string[]? ParseJsonPolicyData(string policyDataJson) + { + string[]? policyData; + try + { + policyData = JsonSerializer.Deserialize(policyDataJson); + } + catch(JsonException ex) + { + return null; + } + + return policyData; + } + + private static bool ValidateThumbnailPolicyData(string policyDataJson) + { + var policyData = ParseJsonPolicyData(policyDataJson); + + if (policyData.IsNullOrEmpty()) + { + return false; + } + + var isInvalid = false; + + foreach (var sizeValue in policyData) + { + try { SizeParameter.Parse(sizeValue); } + catch + { + isInvalid = true; + break; + } + } + + return !isInvalid; + } + + private static bool ValidateTimeBasedPolicyData(string policyDataJson) + { + var policyData = ParseJsonPolicyData(policyDataJson); + + if (policyData.IsNullOrEmpty()) + { + return false; + } + + return validTimeBasedFormats.Contains(policyData[0]); + } +} \ No newline at end of file From ed8307ee64670e50a8e207f6c9da952fb7328b95 Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 16:06:51 +0000 Subject: [PATCH 11/35] Remove image from policy type whitelist, Rename IsValidDeliveryChannelPolicy to IsValidDeliveryChannel --- .../DeliveryChannelPoliciesController.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 7cb4baf94..e95fb4d15 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -22,9 +22,8 @@ public class DeliveryChannelPoliciesController : HydraController { private readonly string[] allowedDeliveryChannels = { - AssetDeliveryChannels.Image, + AssetDeliveryChannels.Thumbnails, AssetDeliveryChannels.Timebased, - AssetDeliveryChannels.Thumbnails }; public DeliveryChannelPoliciesController( @@ -63,9 +62,9 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsValidDeliveryChannelPolicy(deliveryChannelName)) + if(!IsValidDeliveryChannel(deliveryChannelName)) { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); } @@ -114,9 +113,9 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsValidDeliveryChannelPolicy(deliveryChannelName)) + if(!IsValidDeliveryChannel(deliveryChannelName)) { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); } @@ -150,9 +149,9 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsValidDeliveryChannelPolicy(deliveryChannelName)) + if(!IsValidDeliveryChannel(deliveryChannelName)) { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); } @@ -192,7 +191,7 @@ public class DeliveryChannelPoliciesController : HydraController return await HandleDelete(deleteDeliveryChannelPolicy); } - private bool IsValidDeliveryChannelPolicy(string deliveryChannelPolicyName) + private bool IsValidDeliveryChannel(string deliveryChannelPolicyName) { return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); } From 741dc11a76196a5ed137fc1dd5d847e9715e4eac Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 16:19:40 +0000 Subject: [PATCH 12/35] Move delivery channel policy tests into a separate file --- .../Integration/CustomerPolicyTests.cs | 33 ----------- .../Integration/DeliveryChannelTests.cs | 57 +++++++++++++++++++ 2 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs diff --git a/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs b/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs index 3e87ae995..9a2fdc675 100644 --- a/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs +++ b/src/protagonist/API.Tests/Integration/CustomerPolicyTests.cs @@ -93,37 +93,4 @@ public async Task Get_ImageOptimisationPolicy_404_IfNotFound() // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } - - [Fact] - public async Task Get_DeliveryChannelPolicy_200() - { - // Arrange - var path = $"customers/99/deliveryChannelPolicies/thumbs/example-thumbs-policy"; - - // Act - var response = await httpClient.AsCustomer(99).GetAsync(path); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var model = await response.ReadAsHydraResponseAsync(); - model.Name.Should().Be("example-thumbs-policy"); - model.DisplayName.Should().Be("Example Thumbnail Policy"); - model.Channel.Should().Be("thumbs"); - model.PolicyData.Should().Be("{[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]}"); - } - - [Fact] - public async Task Get_DeliveryChannelPolicy_404_IfNotFound() - { - // Arrange - var path = $"customers/99/deliveryChannelPolicies/thumbs/foofoo"; - - // Act - var response = await httpClient.AsCustomer(99).GetAsync(path); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.NotFound); - } - } \ No newline at end of file diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs new file mode 100644 index 000000000..c902bbebc --- /dev/null +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using API.Client; +using API.Tests.Integration.Infrastructure; +using DLCS.HydraModel; +using DLCS.Repository; +using Hydra.Collections; +using Test.Helpers.Integration; +using Test.Helpers.Integration.Infrastructure; + +namespace API.Tests.Integration; + +[Trait("Category", "Integration")] +[Collection(CollectionDefinitions.DatabaseCollection.CollectionName)] +public class DeliveryChannelTests : IClassFixture> +{ + private readonly HttpClient httpClient; + + public DeliveryChannelTests(DlcsDatabaseFixture dbFixture, ProtagonistAppFactory factory) + { + httpClient = factory.ConfigureBasicAuthedIntegrationTestHttpClient(dbFixture, "API-Test"); + dbFixture.CleanUp(); + } + + [Fact] + public async Task Get_DeliveryChannelPolicy_200() + { + // Arrange + var path = $"customers/99/deliveryChannelPolicies/thumbs/example-thumbs-policy"; + + // Act + var response = await httpClient.AsCustomer(99).GetAsync(path); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var model = await response.ReadAsHydraResponseAsync(); + model.Name.Should().Be("example-thumbs-policy"); + model.DisplayName.Should().Be("Example Thumbnail Policy"); + model.Channel.Should().Be("thumbs"); + model.PolicyData.Should().Be("{[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]}"); + } + + [Fact] + public async Task Get_DeliveryChannelPolicy_404_IfNotFound() + { + // Arrange + var path = $"customers/99/deliveryChannelPolicies/thumbs/foofoo"; + + // Act + var response = await httpClient.AsCustomer(99).GetAsync(path); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } +} \ No newline at end of file From a2088c53d71c26ac908ffa76df0f39cfd74d278f Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 16:50:41 +0000 Subject: [PATCH 13/35] Include full URL in delivery channel policy ID --- .../Converters/DeliveryChannelPolicyConverter.cs | 6 ++---- src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs | 9 ++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs index ddcb3f01e..21c1b9b1a 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs @@ -6,12 +6,10 @@ public static class DeliveryChannelPolicyConverter this DLCS.Model.Policies.DeliveryChannelPolicy deliveryChannelPolicy, string baseUrl) { - return new DLCS.HydraModel.DeliveryChannelPolicy(baseUrl) + return new DLCS.HydraModel.DeliveryChannelPolicy(baseUrl, deliveryChannelPolicy.Customer, + deliveryChannelPolicy.Channel, deliveryChannelPolicy.Name) { - CustomerId = deliveryChannelPolicy.Customer, - Name = deliveryChannelPolicy.Name, DisplayName = deliveryChannelPolicy.DisplayName, - Channel = deliveryChannelPolicy.Channel, PolicyData = deliveryChannelPolicy.PolicyData, Created = deliveryChannelPolicy.Created, Modified = deliveryChannelPolicy.Modified, diff --git a/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs b/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs index 2f38a3c63..763be2601 100644 --- a/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs +++ b/src/protagonist/DLCS.HydraModel/DeliveryChannelPolicy.cs @@ -17,9 +17,12 @@ public DeliveryChannelPolicy() { } - public DeliveryChannelPolicy(string baseUrl) + public DeliveryChannelPolicy(string baseUrl, int customerId, string channelName, string name) { - Init(baseUrl, false); + CustomerId = customerId; + Channel = channelName; + Name = name; + Init(baseUrl, true, customerId, channelName, name); } [RdfProperty(Description = "The URL-friendly name of this delivery channel policy.", @@ -49,7 +52,7 @@ public DeliveryChannelPolicy(string baseUrl) [RdfProperty(Description = "The date this policy was last modified.", Range = Names.XmlSchema.DateTime, ReadOnly = true, WriteOnly = false)] - [JsonProperty(Order = 14, PropertyName = "policyModified")] + [JsonProperty(Order = 15, PropertyName = "policyModified")] public DateTime? Modified { get; set; } } From e17aaf85b5c6a901e1436f3a040234b336e28638 Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 16 Feb 2024 17:27:20 +0000 Subject: [PATCH 14/35] Implement /deliveryChannelPolicies and HydraNestedCollection class --- .../DeliveryChannelPoliciesController.cs | 50 +++++++++++++++++-- .../Requests/GetDeliveryChannelPolicies.cs | 42 ++++++++++++++++ .../Collections/HydraNestedCollection.cs | 16 ++++++ 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs create mode 100644 src/protagonist/Hydra/Collections/HydraNestedCollection.cs diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index e95fb4d15..24eaa2575 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -1,13 +1,17 @@ -using API.Features.DeliveryChannelPolicies.Converters; +using System.Collections.Generic; +using API.Features.DeliveryChannelPolicies.Converters; using API.Features.DeliveryChannelPolicies.Requests; using API.Features.DeliveryChannelPolicies.Validation; using API.Infrastructure; using API.Settings; using DLCS.HydraModel; using DLCS.Model.Assets; +using DLCS.Web.Requests; using FluentValidation; +using Hydra.Collections; using MediatR; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -36,10 +40,39 @@ public class DeliveryChannelPoliciesController : HydraController [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetDeliveryChannelPolicyCollections( - [FromRoute] int customerId, - CancellationToken cancellationToken) + [FromRoute] int customerId) { - throw new NotImplementedException(); + var baseUrl = Request.GetDisplayUrl(Request.Path); + + var hydraPolicyCollections = new List>() + { + new(baseUrl, "iif-img" ) + { + Title = "Policies for IIIF Image service delivery", + }, + new(baseUrl, "iif-thumbs") + { + Title = "Policies for thumbnails as IIIF Image Services", + }, + new(baseUrl, "iif-av") + { + Title = "Policies for Audio and Video delivery", + }, + new(baseUrl, "file") + { + Title = "Policies for File delivery", + } + }; + + var result = new HydraCollection>() + { + WithContext = true, + Members = hydraPolicyCollections.ToArray(), + TotalItems = hydraPolicyCollections.Count, + Id = Request.GetJsonLdId() + }; + + return new OkObjectResult(result); } [HttpGet] @@ -50,7 +83,14 @@ public class DeliveryChannelPoliciesController : HydraController [FromRoute] string deliveryChannelName, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var request = new GetDeliveryChannelPolicies(customerId, deliveryChannelName); + + return await HandleListFetch( + request, + p => p.ToHydra(GetUrlRoots().BaseUrl), + errorTitle: "Failed to get delivery channel policies", + cancellationToken: cancellationToken + ); } [HttpPost] diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs new file mode 100644 index 000000000..189ff9caf --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using API.Infrastructure.Requests; +using DLCS.Model.Policies; +using DLCS.Repository; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace API.Features.DeliveryChannelPolicies.Requests; + +public class GetDeliveryChannelPolicies: IRequest>> +{ + public int CustomerId { get; } + public string DeliveryChannelName { get; set; } + + public GetDeliveryChannelPolicies(int customerId, string deliveryChannelName) + { + CustomerId = customerId; + DeliveryChannelName = deliveryChannelName; + } +} + +public class GetDeliveryChannelPoliciesHandler : IRequestHandler>> +{ + private readonly DlcsContext dbContext; + + public GetDeliveryChannelPoliciesHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task>> Handle(GetDeliveryChannelPolicies request, CancellationToken cancellationToken) + { + var deliveryChannelPolicies = await dbContext.DeliveryChannelPolicies + .AsNoTracking() + .Where(p => + p.Customer == request.CustomerId && + p.Channel == request.DeliveryChannelName) + .ToListAsync(cancellationToken); + + return FetchEntityResult>.Success(deliveryChannelPolicies); + } +} \ No newline at end of file diff --git a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs new file mode 100644 index 000000000..37bf4204c --- /dev/null +++ b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Hydra.Collections; + +public class HydraNestedCollection : HydraCollection +{ + public override string Type => "Collection"; + + public HydraNestedCollection(string baseUrl, string id) + { + Id = $"{baseUrl}/{id}"; + } + + [JsonProperty(Order = 10, PropertyName = "title")] + public string? Title { get; set; } +} \ No newline at end of file From 0b3ef46fb123a50f4bf650290dfff0f9924799cf Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 08:54:13 +0000 Subject: [PATCH 15/35] Include ProducesResponseType annotations, check that the parameter to /deliveryChannelPolicies/{deliveryChannelName} is valid --- .../DeliveryChannelPoliciesController.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index 24eaa2575..e2d2222bd 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -11,7 +11,6 @@ using Hydra.Collections; using MediatR; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -46,19 +45,19 @@ public class DeliveryChannelPoliciesController : HydraController var hydraPolicyCollections = new List>() { - new(baseUrl, "iif-img" ) + new(baseUrl, AssetDeliveryChannels.Image) { Title = "Policies for IIIF Image service delivery", }, - new(baseUrl, "iif-thumbs") + new(baseUrl, AssetDeliveryChannels.Thumbnails) { Title = "Policies for thumbnails as IIIF Image Services", }, - new(baseUrl, "iif-av") + new(baseUrl, AssetDeliveryChannels.Timebased) { Title = "Policies for Audio and Video delivery", }, - new(baseUrl, "file") + new(baseUrl, AssetDeliveryChannels.File) { Title = "Policies for File delivery", } @@ -83,6 +82,12 @@ public class DeliveryChannelPoliciesController : HydraController [FromRoute] string deliveryChannelName, CancellationToken cancellationToken) { + if (!AssetDeliveryChannels.All.Contains(deliveryChannelName)) + { + return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, + 400, "Invalid delivery channel"); + } + var request = new GetDeliveryChannelPolicies(customerId, deliveryChannelName); return await HandleListFetch( @@ -95,6 +100,8 @@ public class DeliveryChannelPoliciesController : HydraController [HttpPost] [Route("{deliveryChannelName}")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task PostDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -127,6 +134,8 @@ public class DeliveryChannelPoliciesController : HydraController [HttpGet] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -145,6 +154,9 @@ public class DeliveryChannelPoliciesController : HydraController [HttpPut] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task PutDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -181,6 +193,9 @@ public class DeliveryChannelPoliciesController : HydraController [HttpPatch] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PatchDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, @@ -220,6 +235,8 @@ public class DeliveryChannelPoliciesController : HydraController [HttpDelete] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteDeliveryChannelPolicy( [FromRoute] int customerId, [FromRoute] string deliveryChannelName, From aacf58cb9a30286839286947f2618a4b19139243 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 09:17:13 +0000 Subject: [PATCH 16/35] Rename existing IsValidDeliveryChannel to IsPermittedDeliveryChannel for clarity, turn IsValidDeliveryChannel into a separate method, check isPermittedDeliveryChannel in DELETE --- .../DeliveryChannelPoliciesController.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs index e2d2222bd..92742052b 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs @@ -82,7 +82,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromRoute] string deliveryChannelName, CancellationToken cancellationToken) { - if (!AssetDeliveryChannels.All.Contains(deliveryChannelName)) + if (!IsValidDeliveryChannel(deliveryChannelName)) { return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, 400, "Invalid delivery channel"); @@ -109,7 +109,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsValidDeliveryChannel(deliveryChannelName)) + if(!IsPermittedDeliveryChannel(deliveryChannelName)) { return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); @@ -165,7 +165,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsValidDeliveryChannel(deliveryChannelName)) + if(!IsPermittedDeliveryChannel(deliveryChannelName)) { return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); @@ -204,7 +204,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsValidDeliveryChannel(deliveryChannelName)) + if(!IsPermittedDeliveryChannel(deliveryChannelName)) { return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); @@ -242,6 +242,12 @@ public class DeliveryChannelPoliciesController : HydraController [FromRoute] string deliveryChannelName, [FromRoute] string deliveryChannelPolicyName) { + if(!IsPermittedDeliveryChannel(deliveryChannelName)) + { + return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, + 400, "Invalid delivery channel policy"); + } + var deleteDeliveryChannelPolicy = new DeleteDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName); @@ -249,6 +255,11 @@ public class DeliveryChannelPoliciesController : HydraController } private bool IsValidDeliveryChannel(string deliveryChannelPolicyName) + { + return AssetDeliveryChannels.All.Contains(deliveryChannelPolicyName); + } + + private bool IsPermittedDeliveryChannel(string deliveryChannelPolicyName) { return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); } From 59c542db95b40e3eea91e09a55c13189e7d48bc3 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 10:19:11 +0000 Subject: [PATCH 17/35] Exclude Customer 99 delivery channel policies from deletion, use fully qualified name for DeliveryChannelPolicy, add tests for HydraDeliveryChannelPolicyValidator --- ...ydraDeliveryChannelPolicyValidatorTests.cs | 115 ++++++++++++++++++ .../API.Tests/Integration/CustomerTests.cs | 2 +- .../Integration/DlcsDatabaseFixture.cs | 3 +- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs diff --git a/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs new file mode 100644 index 000000000..fc48ff84b --- /dev/null +++ b/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs @@ -0,0 +1,115 @@ +using System; +using API.Features.DeliveryChannelPolicies.Validation; +using DLCS.HydraModel; +using FluentValidation.TestHelper; + +namespace API.Tests.Features.DeliveryChannelPolicies.Validation; + +public class HydraDeliveryChannelPolicyValidatorTests +{ + private readonly HydraDeliveryChannelPolicyValidator sut; + + public HydraDeliveryChannelPolicyValidatorTests() + { + sut = new HydraDeliveryChannelPolicyValidator(); + } + + [Fact] + public void NewDeliveryChannelPolicy_CannotHave_AssetId() + { + var policy = new DeliveryChannelPolicy() + { + Id = "foo", + }; + var result = sut.TestValidate(policy); + result.ShouldHaveValidationErrorFor(p => p.Id); + } + + [Fact] + public void NewDeliveryChannelPolicy_CannotHave_CustomerId() + { + var policy = new DeliveryChannelPolicy() + { + CustomerId = 1, + }; + var result = sut.TestValidate(policy); + result.ShouldHaveValidationErrorFor(p => p.CustomerId); + } + + [Fact] + public void NewDeliveryChannelPolicy_CannotHave_Channel() + { + var policy = new DeliveryChannelPolicy() + { + Channel = "iif-img" + }; + var result = sut.TestValidate(policy); + result.ShouldHaveValidationErrorFor(p => p.Channel); + } + + [Fact] + public void NewDeliveryChannelPolicy_CannotHave_PolicyCreated() + { + var policy = new DeliveryChannelPolicy() + { + Created = DateTime.UtcNow + }; + var result = sut.TestValidate(policy); + result.ShouldHaveValidationErrorFor(p => p.Created); + } + + [Fact] + public void NewDeliveryChannelPolicy_CannotHave_PolicyModified() + { + var policy = new DeliveryChannelPolicy() + { + Modified = DateTime.UtcNow + }; + var result = sut.TestValidate(policy); + result.ShouldHaveValidationErrorFor(p => p.Modified); + } + + [Fact] + public void NewDeliveryChannelPolicy_Requires_Name_OnPost() + { + var policy = new DeliveryChannelPolicy() + { + Name = null + }; + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "post")); + result.ShouldHaveValidationErrorFor(p => p.Name); + } + + [Fact] + public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPost() + { + var policy = new DeliveryChannelPolicy() + { + PolicyData = null, + }; + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "post")); + result.ShouldHaveValidationErrorFor(p => p.PolicyData); + } + + [Fact] + public void NewDeliveryChannelPolicy_CannotHave_Name_OnPutOrPatch() + { + var policy = new DeliveryChannelPolicy() + { + Name = "my-delivery-channel-policy" + }; + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put-patch")); + result.ShouldHaveValidationErrorFor(p => p.Name); + } + + [Fact] + public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPutOrPatch() + { + var policy = new DeliveryChannelPolicy() + { + PolicyData = null, + }; + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put-patch")); + result.ShouldHaveValidationErrorFor(p => p.PolicyData); + } +} \ No newline at end of file diff --git a/src/protagonist/API.Tests/Integration/CustomerTests.cs b/src/protagonist/API.Tests/Integration/CustomerTests.cs index acac71d93..089f1b71a 100644 --- a/src/protagonist/API.Tests/Integration/CustomerTests.cs +++ b/src/protagonist/API.Tests/Integration/CustomerTests.cs @@ -180,7 +180,7 @@ public async Task NewlyCreatedCustomer_RollsBackSuccessfully_WhenDeliveryChannel }"; var content = new StringContent(customerJson, Encoding.UTF8, "application/json"); - dbContext.DeliveryChannelPolicies.Add(new DeliveryChannelPolicy() + dbContext.DeliveryChannelPolicies.Add(new DLCS.Model.Policies.DeliveryChannelPolicy() { Id = 250, DisplayName = "A default audio policy", diff --git a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs index 7b728db10..c878c054b 100644 --- a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs +++ b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs @@ -50,7 +50,6 @@ public void CleanUp() DbContext.Database.ExecuteSqlRaw("DELETE FROM \"Customers\" WHERE \"Id\" != 99"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"StoragePolicies\" WHERE \"Id\" not in ('default', 'small', 'medium')"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"ThumbnailPolicies\" WHERE \"Id\" != 'default'"); - DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DeliveryChannelPolicies\" WHERE \"Customer\" != 99"); DbContext.Database.ExecuteSqlRaw( "DELETE FROM \"ImageOptimisationPolicies\" WHERE \"Id\" not in ('fast-higher', 'video-max', 'audio-max', 'cust-default')"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"Images\""); @@ -65,7 +64,7 @@ public void CleanUp() DbContext.Database.ExecuteSqlRaw("DELETE FROM \"EntityCounters\" WHERE \"Type\" = 'space' AND \"Customer\" != 99"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"EntityCounters\" WHERE \"Type\" = 'space-images' AND \"Customer\" != 99"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"EntityCounters\" WHERE \"Type\" = 'customer-images' AND \"Scope\" != '99'"); - DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DeliveryChannelPolicies\" WHERE \"Customer\" <> 1"); + DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DeliveryChannelPolicies\" WHERE \"Customer\" not in ('1','99')"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DefaultDeliveryChannels\" WHERE \"Customer\" <> 1"); DbContext.ChangeTracker.Clear(); } From 26ce5bcb21e1e2c0fc7232a677e030686e81ff53 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 15:27:15 +0000 Subject: [PATCH 18/35] Rename PolicyDataValidator to DeliveryChannelPolicyDataValidator, make DeliveryChannelPolicyDataValidator non-static, change iiif-av policyData validation to just require a single string value --- .../Requests/CreateDeliveryChannelPolicy.cs | 6 +++-- .../Requests/PatchDeliveryChannelPolicy.cs | 6 +++-- .../Requests/UpdateDeliveryChannelPolicy.cs | 6 +++-- ... => DeliveryChannelPolicyDataValidator.cs} | 23 ++++++++----------- src/protagonist/API/Startup.cs | 2 ++ 5 files changed, 23 insertions(+), 20 deletions(-) rename src/protagonist/API/Features/DeliveryChannelPolicies/Validation/{PolicyDataValidator.cs => DeliveryChannelPolicyDataValidator.cs} (70%) diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs index 04cf6c507..21ad1de9e 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs @@ -24,10 +24,12 @@ public CreateDeliveryChannelPolicy(int customerId, DeliveryChannelPolicy deliver public class CreateDeliveryChannelPolicyHandler : IRequestHandler> { private readonly DlcsContext dbContext; + private readonly DeliveryChannelPolicyDataValidator policyDataValidator; - public CreateDeliveryChannelPolicyHandler(DlcsContext dbContext) + public CreateDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelPolicyDataValidator policyDataValidator) { this.dbContext = dbContext; + this.policyDataValidator = policyDataValidator; } public async Task> Handle(CreateDeliveryChannelPolicy request, CancellationToken cancellationToken) @@ -45,7 +47,7 @@ public async Task> Handle(CreateDelive WriteResult.Conflict); } - if(!PolicyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) + if(!policyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) { return ModifyEntityResult.Failure( $"'policyData' contains bad JSON or invalid data", diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs index f1b2c0605..0c6615330 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs @@ -32,10 +32,12 @@ public PatchDeliveryChannelPolicy(int customerId, string channel, string name) public class PatchDeliveryChannelPolicyHandler : IRequestHandler> { private readonly DlcsContext dbContext; + private readonly DeliveryChannelPolicyDataValidator policyDataValidator; - public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext) + public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelPolicyDataValidator policyDataValidator) { this.dbContext = dbContext; + this.policyDataValidator = policyDataValidator; } public async Task> Handle(PatchDeliveryChannelPolicy request, @@ -66,7 +68,7 @@ public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext) { existingDeliveryChannelPolicy.PolicyData = request.PolicyData; - if(!PolicyDataValidator.Validate(request.PolicyData, existingDeliveryChannelPolicy.Channel)) + if(!policyDataValidator.Validate(request.PolicyData, existingDeliveryChannelPolicy.Channel)) { return ModifyEntityResult.Failure( $"'policyData' contains bad JSON or invalid data", diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs index b03f09c0d..bb6c10462 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs @@ -24,15 +24,17 @@ public UpdateDeliveryChannelPolicy(int customerId, DeliveryChannelPolicy deliver public class UpdateDeliveryChannelPolicyHandler : IRequestHandler> { private readonly DlcsContext dbContext; + private readonly DeliveryChannelPolicyDataValidator policyDataValidator; - public UpdateDeliveryChannelPolicyHandler(DlcsContext dbContext) + public UpdateDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelPolicyDataValidator policyDataValidator) { this.dbContext = dbContext; + this.policyDataValidator = policyDataValidator; } public async Task> Handle(UpdateDeliveryChannelPolicy request, CancellationToken cancellationToken) { - if(!PolicyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) + if(!policyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) { return ModifyEntityResult.Failure( $"'policyData' contains bad JSON or invalid data", diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs similarity index 70% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs rename to src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs index 8d572a7d9..9bcf5c7b8 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/PolicyDataValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs @@ -5,15 +5,9 @@ namespace API.Features.DeliveryChannelPolicies.Validation; -public static class PolicyDataValidator +public class DeliveryChannelPolicyDataValidator { - private static string[] validTimeBasedFormats = - { - "video-mp4-720p", - "audio-mp3-128" - }; - - public static bool Validate(string policyDataJson, string channel) + public bool Validate(string policyDataJson, string channel) { return channel switch { @@ -23,7 +17,7 @@ public static bool Validate(string policyDataJson, string channel) }; } - private static string[]? ParseJsonPolicyData(string policyDataJson) + private string[]? ParseJsonPolicyData(string policyDataJson) { string[]? policyData; try @@ -38,7 +32,7 @@ public static bool Validate(string policyDataJson, string channel) return policyData; } - private static bool ValidateThumbnailPolicyData(string policyDataJson) + private bool ValidateThumbnailPolicyData(string policyDataJson) { var policyData = ParseJsonPolicyData(policyDataJson); @@ -62,15 +56,16 @@ private static bool ValidateThumbnailPolicyData(string policyDataJson) return !isInvalid; } - private static bool ValidateTimeBasedPolicyData(string policyDataJson) + private bool ValidateTimeBasedPolicyData(string policyDataJson) { var policyData = ParseJsonPolicyData(policyDataJson); - if (policyData.IsNullOrEmpty()) + // For now, we only expect a single string value + if (policyData == null || policyData.Length != 1) { return false; } - - return validTimeBasedFormats.Contains(policyData[0]); + + return true; } } \ No newline at end of file diff --git a/src/protagonist/API/Startup.cs b/src/protagonist/API/Startup.cs index c1af1b4d1..0668bd7b4 100644 --- a/src/protagonist/API/Startup.cs +++ b/src/protagonist/API/Startup.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using API.Auth; +using API.Features.DeliveryChannelPolicies.Validation; using API.Features.Image.Ingest; using API.Features.OriginStrategies.Credentials; using API.Infrastructure; @@ -74,6 +75,7 @@ public void ConfigureServices(IServiceCollection services) .AddSingleton() .AddScoped() .AddTransient() + .AddTransient() .AddValidatorsFromAssemblyContaining() .ConfigureMediatR() .AddNamedQueriesCore() From 9c0de4755cadeea7f790e3f97ca42e1f2631ab6d Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 16:08:16 +0000 Subject: [PATCH 19/35] Add tests for DeliveryChannelPolicyDataValidator, disallow empty iiif-av policy --- ...DeliveryChannelPolicyDataValidatorTests.cs | 120 ++++++++++++++++++ .../DeliveryChannelPolicyDataValidator.cs | 2 +- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs diff --git a/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs new file mode 100644 index 000000000..993ae9980 --- /dev/null +++ b/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs @@ -0,0 +1,120 @@ +using API.Features.DeliveryChannelPolicies.Validation; + +namespace API.Tests.Features.DeliveryChannelPolicies.Validation; + +public class DeliveryChannelPolicyDataValidatorTests +{ + private readonly DeliveryChannelPolicyDataValidator sut; + + public DeliveryChannelPolicyDataValidatorTests() + { + sut = new DeliveryChannelPolicyDataValidator(); + } + + [Theory] + [InlineData("[\"400,400\",\"200,200\",\"100,100\"]")] + [InlineData("[\"!400,400\",\"!200,200\",\"!100,100\"]")] + [InlineData("[\"400,\",\"200,\",\"100,\"]")] + [InlineData("[\"!400,\",\"!200,\",\"!100,\"]")] + [InlineData("[\"400,400\"]")] + public void PolicyDataValidator_ReturnsTrue_ForValidThumbSizes(string policyData) + { + // Arrange And Act + var result = sut.Validate(policyData, "thumbs"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void PolicyDataValidator_ReturnsFalse_ForBadThumbSizes() + { + // Arrange + var policyData = "[\"400,400\",\"foo,bar\",\"100,100\"]"; + + // Act + var result = sut.Validate(policyData, "thumbs"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void PolicyDataValidator_ReturnsFalse_ForInvalidThumbSizesJson() + { + // Arrange + var policyData = "[\"400,400\","; + + // Act + var result = sut.Validate(policyData, "thumbs"); + + // Assert + result.Should().BeFalse(); + } + + [Theory] + [InlineData("")] + [InlineData("[]")] + [InlineData("[\"\"]")] + public void PolicyDataValidator_ReturnsFalse_ForEmptyThumbSizes(string policyData) + { + // Arrange and Act + var result = sut.Validate(policyData, "thumbs"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void PolicyDataValidator_ReturnsTrue_ForValidAvPolicy() + { + // Arrange + var policyData = "[\"media-format-quality\"]"; // For now, any single string value is accepted - this will need + // to be rewritten once the API requires a valid transcoder policy + + // Act + var result = sut.Validate(policyData, "iiif-av"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void PolicyDataValidator_ReturnsFalse_ForBadAvPolicy() + { + // Arrange + var policyData = "[\"policy-1\",\"policy-2\"]"; + + // Arrange and Act + var result = sut.Validate(policyData, "iiif-av"); + + // Assert + result.Should().BeFalse(); + } + + [Theory] + [InlineData("")] + [InlineData("[]")] + [InlineData("[\"\"]")] + public void PolicyDataValidator_ReturnsFalse_ForEmptyAvPolicy(string policyData) + { + // Arrange and Act + var result = sut.Validate(policyData, "iiif-av"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void PolicyDataValidator_ReturnsFalse_ForInvalidAvPolicyJson() + { + // Arrange + var policyData = "[\"policy-1\","; + + // Act + var result = sut.Validate(policyData, "iiif-av"); + + // Assert + result.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs index 9bcf5c7b8..8e9c253c3 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs @@ -61,7 +61,7 @@ private bool ValidateTimeBasedPolicyData(string policyDataJson) var policyData = ParseJsonPolicyData(policyDataJson); // For now, we only expect a single string value - if (policyData == null || policyData.Length != 1) + if (policyData == null || policyData.Length != 1 || string.IsNullOrEmpty((policyData[0]))) { return false; } From 96c7bd48d1db323393d5e6b601ce41529df729e7 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 16:17:03 +0000 Subject: [PATCH 20/35] Rename DeliveryChannelPolicies folders to DeliveryChannels --- .../Validation/DeliveryChannelPolicyDataValidatorTests.cs | 2 +- .../HydraDeliveryChannelPolicyValidatorTests.cs | 2 +- .../Converters/DeliveryChannelPolicyConverter.cs | 2 +- .../DeliveryChannelPoliciesController.cs | 8 ++++---- .../Requests/CreateDeliveryChannelPolicy.cs | 4 ++-- .../Requests/DeleteDeliveryChannelPolicy.cs | 2 +- .../Requests/GetDeliveryChannelPolicies.cs | 2 +- .../Requests/GetDeliveryChannelPolicy.cs | 2 +- .../Requests/PatchDeliveryChannelPolicy.cs | 4 ++-- .../Requests/UpdateDeliveryChannelPolicy.cs | 4 ++-- .../Validation/DeliveryChannelPolicyDataValidator.cs | 2 +- .../Validation/HydraDeliveryChannelPolicyValidator.cs | 2 +- src/protagonist/API/Startup.cs | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) rename src/protagonist/API.Tests/Features/{DeliveryChannelPolicies => DeliveryChannels}/Validation/DeliveryChannelPolicyDataValidatorTests.cs (98%) rename src/protagonist/API.Tests/Features/{DeliveryChannelPolicies => DeliveryChannels}/Validation/HydraDeliveryChannelPolicyValidatorTests.cs (98%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Converters/DeliveryChannelPolicyConverter.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/DeliveryChannelPoliciesController.cs (98%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Requests/CreateDeliveryChannelPolicy.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Requests/DeleteDeliveryChannelPolicy.cs (97%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Requests/GetDeliveryChannelPolicies.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Requests/GetDeliveryChannelPolicy.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Requests/PatchDeliveryChannelPolicy.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Requests/UpdateDeliveryChannelPolicy.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Validation/DeliveryChannelPolicyDataValidator.cs (96%) rename src/protagonist/API/Features/{DeliveryChannelPolicies => DeliveryChannels}/Validation/HydraDeliveryChannelPolicyValidator.cs (96%) diff --git a/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs similarity index 98% rename from src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs rename to src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs index 993ae9980..5990e5e1a 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs @@ -1,4 +1,4 @@ -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Validation; namespace API.Tests.Features.DeliveryChannelPolicies.Validation; diff --git a/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs similarity index 98% rename from src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs rename to src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs index fc48ff84b..4db3bbb04 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs @@ -1,5 +1,5 @@ using System; -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Validation; using DLCS.HydraModel; using FluentValidation.TestHelper; diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs b/src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs rename to src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs index 21c1b9b1a..62e4326f3 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Converters/DeliveryChannelPolicyConverter.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs @@ -1,4 +1,4 @@ -namespace API.Features.DeliveryChannelPolicies.Converters; +namespace API.Features.DeliveryChannels.Converters; public static class DeliveryChannelPolicyConverter { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs similarity index 98% rename from src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs rename to src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index 92742052b..a5567a482 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Features.DeliveryChannelPolicies.Converters; -using API.Features.DeliveryChannelPolicies.Requests; -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Converters; +using API.Features.DeliveryChannels.Requests; +using API.Features.DeliveryChannels.Validation; using API.Infrastructure; using API.Settings; using DLCS.HydraModel; @@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -namespace API.Features.DeliveryChannelPolicies; +namespace API.Features.DeliveryChannels; /// /// DLCS REST API Operations for delivery channel policies diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs index 21ad1de9e..61a212345 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs @@ -1,4 +1,4 @@ -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Validation; using API.Infrastructure.Requests; using DLCS.Core; using DLCS.Model.Policies; @@ -6,7 +6,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace API.Features.DeliveryChannelPolicies.Requests; +namespace API.Features.DeliveryChannels.Requests; public class CreateDeliveryChannelPolicy : IRequest> { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs similarity index 97% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs index 16145246f..ba46db874 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/DeleteDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs @@ -4,7 +4,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace API.Features.DeliveryChannelPolicies.Requests; +namespace API.Features.DeliveryChannels.Requests; public class DeleteDeliveryChannelPolicy: IRequest> { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs index 189ff9caf..acef5ed80 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicies.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs @@ -5,7 +5,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace API.Features.DeliveryChannelPolicies.Requests; +namespace API.Features.DeliveryChannels.Requests; public class GetDeliveryChannelPolicies: IRequest>> { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs index 03e54926d..929ffc7d5 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/GetDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs @@ -4,7 +4,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace API.Features.DeliveryChannelPolicies.Requests; +namespace API.Features.DeliveryChannels.Requests; public class GetDeliveryChannelPolicy: IRequest> { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs index 0c6615330..4b815aa1a 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/PatchDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs @@ -1,4 +1,4 @@ -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Validation; using API.Infrastructure.Requests; using DLCS.Core; using DLCS.Core.Strings; @@ -7,7 +7,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace API.Features.DeliveryChannelPolicies.Requests; +namespace API.Features.DeliveryChannels.Requests; public class PatchDeliveryChannelPolicy : IRequest> { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs index bb6c10462..00e99f90a 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Requests/UpdateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs @@ -1,4 +1,4 @@ -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Validation; using API.Infrastructure.Requests; using DLCS.Core; using DLCS.Model.Policies; @@ -6,7 +6,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; -namespace API.Features.DeliveryChannelPolicies.Requests; +namespace API.Features.DeliveryChannels.Requests; public class UpdateDeliveryChannelPolicy : IRequest> { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs rename to src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs index 8e9c253c3..ed2d1096c 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/DeliveryChannelPolicyDataValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs @@ -3,7 +3,7 @@ using DLCS.Model.Assets; using IIIF.ImageApi; -namespace API.Features.DeliveryChannelPolicies.Validation; +namespace API.Features.DeliveryChannels.Validation; public class DeliveryChannelPolicyDataValidator { diff --git a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs similarity index 96% rename from src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs rename to src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs index 8f8ca9c1e..f2acf6335 100644 --- a/src/protagonist/API/Features/DeliveryChannelPolicies/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -namespace API.Features.DeliveryChannelPolicies.Validation; +namespace API.Features.DeliveryChannels.Validation; /// /// Validator for model sent to POST /deliveryChannelPolicies and PUT/PATCH /deliveryChannelPolicies/{id} diff --git a/src/protagonist/API/Startup.cs b/src/protagonist/API/Startup.cs index 0668bd7b4..d34c72c0d 100644 --- a/src/protagonist/API/Startup.cs +++ b/src/protagonist/API/Startup.cs @@ -1,6 +1,6 @@ using System.Security.Claims; using API.Auth; -using API.Features.DeliveryChannelPolicies.Validation; +using API.Features.DeliveryChannels.Validation; using API.Features.Image.Ingest; using API.Features.OriginStrategies.Credentials; using API.Infrastructure; From 026d97e9bd756f1ede995fd753a0a66504db94c7 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 16:44:57 +0000 Subject: [PATCH 21/35] Fix double slash in HydraNestedCollection Id --- src/protagonist/Hydra/Collections/HydraNestedCollection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs index 37bf4204c..76df6ac64 100644 --- a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs +++ b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Hydra.Collections; @@ -8,7 +9,7 @@ public class HydraNestedCollection : HydraCollection public HydraNestedCollection(string baseUrl, string id) { - Id = $"{baseUrl}/{id}"; + Id = new Uri(new Uri(baseUrl), id).ToString(); } [JsonProperty(Order = 10, PropertyName = "title")] From cb073f4631c26d9f24a8d890e9139009629715a9 Mon Sep 17 00:00:00 2001 From: griffri Date: Wed, 21 Feb 2024 17:46:14 +0000 Subject: [PATCH 22/35] Add POST, PUT, PATCH and DELETE tests --- .../Integration/DeliveryChannelTests.cs | 144 +++++++++++++++++- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index c902bbebc..ac7bbb656 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -1,11 +1,12 @@ -using System.Net; +using System.Linq; +using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using API.Client; using API.Tests.Integration.Infrastructure; using DLCS.HydraModel; using DLCS.Repository; -using Hydra.Collections; using Test.Helpers.Integration; using Test.Helpers.Integration.Infrastructure; @@ -16,9 +17,11 @@ namespace API.Tests.Integration; public class DeliveryChannelTests : IClassFixture> { private readonly HttpClient httpClient; - - public DeliveryChannelTests(DlcsDatabaseFixture dbFixture, ProtagonistAppFactory factory) + private readonly DlcsContext dbContext; + + public DeliveryChannelTests(DlcsDatabaseFixture dbFixture, ProtagonistAppFactory factory) { + dbContext = dbFixture.DbContext; httpClient = factory.ConfigureBasicAuthedIntegrationTestHttpClient(dbFixture, "API-Test"); dbFixture.CleanUp(); } @@ -54,4 +57,137 @@ public async Task Get_DeliveryChannelPolicy_404_IfNotFound() // Assert response.StatusCode.Should().Be(HttpStatusCode.NotFound); } + + [Fact] + public async Task Post_DeliveryChannelPolicy_201() + { + // Arrange + const int customerId = 88; + const string newDeliveryChannelPolicyJson = @"{ + ""name"": ""my-iiif-av-policy-1"", + ""displayName"": ""My IIIF AV Policy"", + ""policyData"": ""[\""audio-mp3-128\""]"" + }"; + + var path = $"customers/{customerId}/deliveryChannelPolicies/iiif-av"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + + var foundPolicy = dbContext.DeliveryChannelPolicies.Single(s => + s.Customer == customerId && + s.Name == "my-iiif-av-policy-1"); + foundPolicy.DisplayName.Should().Be("My IIIF AV Policy"); + foundPolicy.PolicyData.Should().Be("[\"audio-mp3-128\"]"); + } + + [Fact] + public async Task Put_DeliveryChannelPolicy_201() + { + // Arrange + const int customerId = 88; + const string putDeliveryChannelPolicyJson = @"{ + ""displayName"": ""My IIIF AV Policy 2 (modified)"", + ""policyData"": ""[\""audio-mp3-256\""]"" + }"; + + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "put-av-policy-2", + DisplayName = "My IIIF-AV Policy 2", + Channel = "iiif-av", + PolicyData = "[\"audio-mp3-128\"]" + }; + + var path = $"customers/{customerId}/deliveryChannelPolicies/{policy.Channel}/{policy.Name}"; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(putDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PutAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var foundPolicy = dbContext.DeliveryChannelPolicies.Single(s => + s.Customer == customerId && + s.Name == policy.Name); + foundPolicy.DisplayName.Should().Be("My IIIF AV Policy 2 (modified)"); + foundPolicy.PolicyData.Should().Be("[\"audio-mp3-256\"]"); + } + + + [Fact] + public async Task Patch_DeliveryChannelPolicy_201() + { + // Arrange + const int customerId = 102; + const string patchDeliveryChannelPolicyJson = @"{ + ""displayName"": ""My IIIF AV Policy 3 (modified)"", + ""policyData"": ""[\""audio-mp3-256\""]"" + }"; + + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "put-av-policy-3", + DisplayName = "My IIIF-AV Policy 3", + Channel = "iiif-av", + PolicyData = "[\"audio-mp3-128\"]" + }; + + var path = $"customers/{customerId}/deliveryChannelPolicies/{policy.Channel}/{policy.Name}"; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(patchDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PatchAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var foundPolicy = dbContext.DeliveryChannelPolicies.Single(s => + s.Customer == customerId && + s.Name == policy.Name); + foundPolicy.DisplayName.Should().Be("My IIIF AV Policy 3 (modified)"); + foundPolicy.PolicyData.Should().Be("[\"audio-mp3-256\"]"); + } + + [Fact] + public async Task Delete_DeliveryChannelPolicy_204() + { + // Arrange + const int customerId = 102; + + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "delete-thumbs-policy", + DisplayName = "My Thumbs Policy", + Channel = "thumbs", + PolicyData = "[\"!100,100\"]", + }; + var path = $"customers/{customerId}/deliveryChannelPolicies/{policy.Channel}/{policy.Name}"; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var response = await httpClient.AsCustomer(customerId).DeleteAsync(path); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + var strategyExists = dbContext.DeliveryChannelPolicies.Any(p => p.Name == policy.Name); + strategyExists.Should().BeFalse(); + } } \ No newline at end of file From 7ae99211681d6f11fb96972a97a1fcbf5cf884d7 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 08:11:33 +0000 Subject: [PATCH 23/35] Add delivery channel policy collection tests, fix invalid policyData in seeded delivery channel policy, Add invalid PolicyData tests --- .../Integration/DeliveryChannelTests.cs | 212 +++++++++++++++++- .../Collections/HydraNestedCollection.cs | 5 + .../Integration/DlcsDatabaseFixture.cs | 2 +- 3 files changed, 210 insertions(+), 9 deletions(-) diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index ac7bbb656..21697b98d 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -7,6 +8,7 @@ using API.Tests.Integration.Infrastructure; using DLCS.HydraModel; using DLCS.Repository; +using Hydra.Collections; using Test.Helpers.Integration; using Test.Helpers.Integration.Infrastructure; @@ -42,14 +44,14 @@ public async Task Get_DeliveryChannelPolicy_200() model.Name.Should().Be("example-thumbs-policy"); model.DisplayName.Should().Be("Example Thumbnail Policy"); model.Channel.Should().Be("thumbs"); - model.PolicyData.Should().Be("{[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]}"); + model.PolicyData.Should().Be("[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]"); } [Fact] public async Task Get_DeliveryChannelPolicy_404_IfNotFound() { // Arrange - var path = $"customers/99/deliveryChannelPolicies/thumbs/foofoo"; + var path = $"customers/99/deliveryChannelPolicies/thumbs/foo"; // Act var response = await httpClient.AsCustomer(99).GetAsync(path); @@ -86,7 +88,80 @@ public async Task Post_DeliveryChannelPolicy_201() } [Fact] - public async Task Put_DeliveryChannelPolicy_201() + public async Task Post_DeliveryChannelPolicy_400_IfChannelInvalid() + { + // Arrange + const int customerId = 88; + const string newDeliveryChannelPolicyJson = @"{ + ""name"": ""post-invalid-policy"", + ""displayName"": ""Invalid Policy"", + ""policyData"": ""[\""audio-mp3-128\""]"" + }"; + + var path = $"customers/{customerId}/deliveryChannelPolicies/foo"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Theory] + [InlineData("")] // No PolicyData specified + [InlineData("[]")] // Empty array + [InlineData("[\"\"]")] // Array containing an empty value + [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""100,100\"",\""200,200\""")] // Invalid JSON + public async Task Post_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(string policyData) + { + // Arrange + const int customerId = 88; + + var newDeliveryChannelPolicyJson = $@"{{ + ""name"": ""post-invalid-thumbs"", + ""displayName"": ""Invalid Policy (Thumbs Policy Data)"", + ""policyData"": ""{policyData}"" + }}"; + var path = $"customers/{customerId}/deliveryChannelPolicies/thumbs"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Theory] + [InlineData("")] // No PolicyData specified + [InlineData("[]")] // Empty array + [InlineData("[\"\"]")] // Array containing an empty value + [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""transcode-policy\""")] // Invalid JSON + public async Task Post_DeliveryChannelPolicy_400_IfAvPolicyDataInvalid(string policyData) + { + // Arrange + const int customerId = 88; + + var newDeliveryChannelPolicyJson = $@"{{ + ""name"": ""post-invalid-iiif-av"", + ""displayName"": ""Invalid Policy (IIIF-AV Policy Data)"", + ""policyData"": ""{policyData}"" + }}"; + var path = $"customers/{customerId}/deliveryChannelPolicies/iiif-av"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Put_DeliveryChannelPolicy_200() { // Arrange const int customerId = 88; @@ -123,12 +198,83 @@ public async Task Put_DeliveryChannelPolicy_201() foundPolicy.PolicyData.Should().Be("[\"audio-mp3-256\"]"); } + [Theory] + [InlineData("")] // No PolicyData specified + [InlineData("[]")] // Empty array + [InlineData("[\"\"]")] // Array containing an empty value + [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""100,100\"",\""200,200\""")] // Invalid JSON + public async Task Put_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(string policyData) + { + // Arrange + const int customerId = 88; + var newDeliveryChannelPolicyJson = $@"{{ + ""displayName"": ""Invalid Policy (Thumbs Policy Data)"", + ""policyData"": ""{policyData}"" + }}"; + var path = $"customers/{customerId}/deliveryChannelPolicies/thumbs/put-invalid-thumbs"; + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "put-invalid-thumbs", + DisplayName = "Valid Policy (Thumbs Policy Data)", + Channel = "thumbs", + PolicyData = "[\"100,100\"]" + }; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PutAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Theory] + [InlineData("")] // No PolicyData specified + [InlineData("[]")] // Empty array + [InlineData("[\"\"]")] // Array containing an empty value + [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""transcode-policy\""")] // Invalid JSON + public async Task Put_DeliveryChannelPolicy_400_IfAvPolicyDataInvalid(string policyData) + { + // Arrange + const int customerId = 88; + + var newDeliveryChannelPolicyJson = $@"{{ + ""displayName"": ""Invalid Policy (IIIF-AV Policy Data)"", + ""policyData"": ""{policyData}"" + }}"; + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "put-invalid-iiif-av", + DisplayName = "Valid Policy (IIIF-AV Policy Data)", + Channel = "thumbs", + PolicyData = "[\"100,100\"]" + }; + var path = $"customers/{customerId}/deliveryChannelPolicies/iiif-av/put-invalid-iiif-av"; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PutAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + [Fact] public async Task Patch_DeliveryChannelPolicy_201() { // Arrange - const int customerId = 102; + const int customerId = 88; const string patchDeliveryChannelPolicyJson = @"{ ""displayName"": ""My IIIF AV Policy 3 (modified)"", ""policyData"": ""[\""audio-mp3-256\""]"" @@ -166,7 +312,7 @@ public async Task Patch_DeliveryChannelPolicy_201() public async Task Delete_DeliveryChannelPolicy_204() { // Arrange - const int customerId = 102; + const int customerId = 88; var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() { @@ -187,7 +333,57 @@ public async Task Delete_DeliveryChannelPolicy_204() // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); - var strategyExists = dbContext.DeliveryChannelPolicies.Any(p => p.Name == policy.Name); - strategyExists.Should().BeFalse(); + var policyExists = dbContext.DeliveryChannelPolicies.Any(p => p.Name == policy.Name); + policyExists.Should().BeFalse(); + } + + [Fact] + public async Task Get_DeliveryChannelPolicyCollections_200() + { + // Arrange + const int customerId = 88; + + // Act + var response = await httpClient.AsCustomer(customerId).GetAsync($"customers/{customerId}/deliveryChannelPolicies"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var collections = await response.ReadAsHydraResponseAsync>>(); + collections.TotalItems.Should().Be(4); // Should contain iiif-img, thumbs, iiif-av and file + } + + [Fact] + public async Task Get_DeliveryChannelPolicyCollection_200() + { + // Arrange + const int customerId = 99; + + // Act + var response = await httpClient.AsCustomer(customerId).GetAsync($"customers/{customerId}/deliveryChannelPolicies/thumbs"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var collection = await response.ReadAsHydraResponseAsync>(); + collection.TotalItems.Should().Be(1); + + var createdPolicy = collection.Members.FirstOrDefault(); + createdPolicy.Name.Should().Be("example-thumbs-policy"); + createdPolicy.Channel.Should().Be("thumbs"); + createdPolicy.PolicyData.Should().Be("[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]"); + } + + [Fact] + public async Task Get_DeliveryChannelPolicyCollection_400_IfChannelInvalid() + { + // Arrange + const int customerId = 88; + + // Act + var response = await httpClient.AsCustomer(customerId).GetAsync($"customers/{customerId}/deliveryChannelPolicies/foo"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } } \ No newline at end of file diff --git a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs index 76df6ac64..22d629695 100644 --- a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs +++ b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs @@ -6,6 +6,11 @@ namespace Hydra.Collections; public class HydraNestedCollection : HydraCollection { public override string Type => "Collection"; + + public HydraNestedCollection() + { + + } public HydraNestedCollection(string baseUrl, string id) { diff --git a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs index c878c054b..56d7aed16 100644 --- a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs +++ b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs @@ -170,7 +170,7 @@ await DbContext.DeliveryChannelPolicies.AddAsync(new DeliveryChannelPolicy() Name = "example-thumbs-policy", DisplayName = "Example Thumbnail Policy", Channel = "thumbs", - PolicyData = "{[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]}", + PolicyData = "[\"!1024,1024\",\"!400,400\",\"!200,200\",\"!100,100\"]", System = false, }); await DbContext.AuthServices.AddAsync(new AuthService From 82d970bf8ecec9ebce605b7c19789f627807a7d7 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 09:06:28 +0000 Subject: [PATCH 24/35] Add Patch invalid policy data, Delete_404_IfNotFound and Put_400_IfChannelInvalid tests --- .../Integration/DeliveryChannelTests.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index 21697b98d..65eedd5ff 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -198,6 +198,26 @@ public async Task Put_DeliveryChannelPolicy_200() foundPolicy.PolicyData.Should().Be("[\"audio-mp3-256\"]"); } + [Fact] + public async Task Put_DeliveryChannelPolicy_400_IfChannelInvalid() + { + // Arrange + const int customerId = 88; + const string newDeliveryChannelPolicyJson = @"{ + ""displayName"": ""Invalid Policy"", + ""policyData"": ""[\""audio-mp3-128\""]"" + }"; + + var path = $"customers/{customerId}/deliveryChannelPolicies/foo/put-invalid-channel-policy"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PutAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + [Theory] [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array @@ -308,6 +328,76 @@ public async Task Patch_DeliveryChannelPolicy_201() foundPolicy.PolicyData.Should().Be("[\"audio-mp3-256\"]"); } + [Theory] + [InlineData("")] // No PolicyData specified + [InlineData("[]")] // Empty array + [InlineData("[\"\"]")] // Array containing an empty value + [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""100,100\"",\""200,200\""")] // Invalid JSON + public async Task Patch_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(string policyData) + { + // Arrange + const int customerId = 88; + + var newDeliveryChannelPolicyJson = $@"{{ + ""policyData"": ""{policyData}"" + }}"; + var path = $"customers/{customerId}/deliveryChannelPolicies/thumbs/patch-invalid-thumbs"; + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "patch-invalid-thumbs", + DisplayName = "Valid Policy (Thumbs Policy Data)", + Channel = "thumbs", + PolicyData = "[\"100,100\"]" + }; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PatchAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Theory] + [InlineData("")] // No PolicyData specified + [InlineData("[]")] // Empty array + [InlineData("[\"\"]")] // Array containing an empty value + [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""transcode-policy\""")] // Invalid JSON + public async Task Patch_DeliveryChannelPolicy_400_IfAvPolicyDataInvalid(string policyData) + { + // Arrange + const int customerId = 88; + + var newDeliveryChannelPolicyJson = $@"{{ + ""policyData"": ""{policyData}"" + }}"; + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "patch-invalid-iiif-av", + DisplayName = "Valid Policy (IIIF-AV Policy Data)", + Channel = "iiif-av", + PolicyData = "[\"100,100\"]" + }; + var path = $"customers/{customerId}/deliveryChannelPolicies/iiif-av/patch-invalid-iiif-av"; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PatchAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + [Fact] public async Task Delete_DeliveryChannelPolicy_204() { @@ -337,6 +427,20 @@ public async Task Delete_DeliveryChannelPolicy_204() policyExists.Should().BeFalse(); } + [Fact] + public async Task Delete_DeliveryChannelPolicy_404_IfNotFound() + { + // Arrange + const int customerId = 88; + var path = $"customers/{customerId}/deliveryChannelPolicies/thumbs/foo"; + + // Act + var response = await httpClient.AsCustomer(customerId).DeleteAsync(path); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + [Fact] public async Task Get_DeliveryChannelPolicyCollections_200() { From 8fc766aff2bd2728bd8378736d1c3a4ef4f97511 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 11:22:50 +0000 Subject: [PATCH 25/35] Ensure that delivery channel policy names don't contain whitespace or capital letters, add tests that ensure this rule is enforced --- .../Integration/DeliveryChannelTests.cs | 43 ++++++++++++++++++- .../DeliveryChannelPoliciesController.cs | 29 ++++++++++++- .../HydraDeliveryChannelPolicyValidator.cs | 3 +- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index 65eedd5ff..ce8f3a40f 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -108,6 +108,27 @@ public async Task Post_DeliveryChannelPolicy_400_IfChannelInvalid() response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } + [Fact] + public async Task Post_DeliveryChannelPolicy_400_IfNameInvalid() + { + // Arrange + const int customerId = 88; + const string newDeliveryChannelPolicyJson = @"{ + ""name"": ""foo bar"", + ""displayName"": ""Invalid Policy"", + ""policyData"": ""[\""audio-mp3-128\""]"" + }"; + + var path = $"customers/{customerId}/deliveryChannelPolicies/iiif-av"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + [Theory] [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array @@ -218,6 +239,26 @@ public async Task Put_DeliveryChannelPolicy_400_IfChannelInvalid() response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } + [Fact] + public async Task Put_DeliveryChannelPolicy_400_IfNameInvalid() + { + // Arrange + const int customerId = 88; + const string newDeliveryChannelPolicyJson = @"{ + ""displayName"": ""Invalid Policy"", + ""policyData"": ""[\""audio-mp3-128\""]""r + }"; + + var path = $"customers/{customerId}/deliveryChannelPolicies/iiif-av/FooBar"; + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PutAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + [Theory] [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array @@ -303,7 +344,7 @@ public async Task Patch_DeliveryChannelPolicy_201() var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() { Customer = customerId, - Name = "put-av-policy-3", + Name = "put-av-policy", DisplayName = "My IIIF-AV Policy 3", Channel = "iiif-av", PolicyData = "[\"audio-mp3-128\"]" diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index a5567a482..2866eab09 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.RegularExpressions; using API.Features.DeliveryChannels.Converters; using API.Features.DeliveryChannels.Requests; using API.Features.DeliveryChannels.Validation; @@ -115,6 +116,12 @@ public class DeliveryChannelPoliciesController : HydraController 400, "Invalid delivery channel policy"); } + if (!IsValidName(hydraDeliveryChannelPolicy.Name)) + { + return this.HydraProblem($"'The name specified for this delivery channel policy is invalid", null, + 400, "Invalid delivery channel policy"); + } + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "post"), cancellationToken); if (!validationResult.IsValid) @@ -170,14 +177,20 @@ public class DeliveryChannelPoliciesController : HydraController return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); } - + + if (!IsValidName(deliveryChannelPolicyName)) + { + return this.HydraProblem($"'The name specified for this delivery channel policy is invalid", null, + 400, "Invalid delivery channel policy"); + } + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "put-patch"), cancellationToken); if (!validationResult.IsValid) { return this.ValidationFailed(validationResult); } - + hydraDeliveryChannelPolicy.CustomerId = customerId; hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; hydraDeliveryChannelPolicy.Channel = deliveryChannelName; @@ -210,6 +223,12 @@ public class DeliveryChannelPoliciesController : HydraController 400, "Invalid delivery channel policy"); } + if (!IsValidName(deliveryChannelPolicyName)) + { + return this.HydraProblem($"The name specified for this delivery channel policy is invalid", null, + 400, "Invalid delivery channel policy"); + } + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "put-patch"), cancellationToken); if (!validationResult.IsValid) @@ -263,4 +282,10 @@ private bool IsPermittedDeliveryChannel(string deliveryChannelPolicyName) { return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); } + + private bool IsValidName(string? inputName) + { + const string regex = "[\\sA-Z]"; // Delivery channel policy names should not contain capital letters or spaces + return !(string.IsNullOrEmpty(inputName) || Regex.IsMatch(inputName, regex)); + } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs index f2acf6335..57d7539d8 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using System.Text.RegularExpressions; +using FluentValidation; namespace API.Features.DeliveryChannels.Validation; From e2398f3f2fca160578a6f6aaa0349d7d0357f804 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 13:38:28 +0000 Subject: [PATCH 26/35] Fix issue that caused parent of a nested collection to be missing in its Id (e.g customer/27/iiif-img would appear instead of customer/27/deliveryChannelPolicies/iiif-img), Null coalesce Created and Modified in HydraDeliveryChannelPolicy.ToDlcsModel --- .../Converters/DeliveryChannelPolicyConverter.cs | 8 ++------ .../Hydra/Collections/HydraNestedCollection.cs | 5 ++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs b/src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs index 62e4326f3..66f87f73a 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Converters/DeliveryChannelPolicyConverter.cs @@ -26,12 +26,8 @@ public static class DeliveryChannelPolicyConverter DisplayName = hydraDeliveryChannelPolicy.DisplayName, Channel = hydraDeliveryChannelPolicy.Channel, PolicyData = hydraDeliveryChannelPolicy.PolicyData, - Created = hydraDeliveryChannelPolicy.Created.HasValue // find a better way to deal with these - ? hydraDeliveryChannelPolicy.Created.Value - : DateTime.MinValue, - Modified = hydraDeliveryChannelPolicy.Modified.HasValue - ? hydraDeliveryChannelPolicy.Modified.Value - : DateTime.MinValue, + Created = hydraDeliveryChannelPolicy.Created ?? DateTime.MinValue, + Modified = hydraDeliveryChannelPolicy.Modified ?? DateTime.MinValue }; } } \ No newline at end of file diff --git a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs index 22d629695..a058d0097 100644 --- a/src/protagonist/Hydra/Collections/HydraNestedCollection.cs +++ b/src/protagonist/Hydra/Collections/HydraNestedCollection.cs @@ -1,5 +1,4 @@ -using System; -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Hydra.Collections; @@ -14,7 +13,7 @@ public HydraNestedCollection() public HydraNestedCollection(string baseUrl, string id) { - Id = new Uri(new Uri(baseUrl), id).ToString(); + Id = $"{baseUrl}/{id}"; } [JsonProperty(Order = 10, PropertyName = "title")] From 722a81da679cd8502fb2be1325a3dfb9bfc2d775 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 15:44:13 +0000 Subject: [PATCH 27/35] Separate PUT and PATCH validation rules for HydraDeliveryChannelPolicy --- .../DeliveryChannels/DeliveryChannelPoliciesController.cs | 4 ++-- .../Validation/HydraDeliveryChannelPolicyValidator.cs | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index 2866eab09..74cdc456f 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -185,7 +185,7 @@ public class DeliveryChannelPoliciesController : HydraController } var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, - policy => policy.IncludeRuleSets("default", "put-patch"), cancellationToken); + policy => policy.IncludeRuleSets("default", "put"), cancellationToken); if (!validationResult.IsValid) { return this.ValidationFailed(validationResult); @@ -230,7 +230,7 @@ public class DeliveryChannelPoliciesController : HydraController } var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, - policy => policy.IncludeRuleSets("default", "put-patch"), cancellationToken); + policy => policy.IncludeRuleSets("default", "patch"), cancellationToken); if (!validationResult.IsValid) { return this.ValidationFailed(validationResult); diff --git a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs index 57d7539d8..bec7fa42b 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -25,7 +25,7 @@ public HydraDeliveryChannelPolicyValidator() .NotEmpty() .WithMessage("'policyData' is required"); }); - RuleSet("put-patch", () => + RuleSet("put", () => { RuleFor(p => p.Name) .Empty() @@ -34,6 +34,12 @@ public HydraDeliveryChannelPolicyValidator() .NotEmpty() .WithMessage("'policyData' is required"); }); + RuleSet("patch", () => + { + RuleFor(p => p.Name) + .Empty() + .WithMessage("'name' should be set in the URL"); + }); RuleFor(p => p.Channel) .Empty() .WithMessage("'channel' should be set in the URL"); From 6ef8eadfca3000b436def5d6cb8d1fab16ed46a0 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 16:20:29 +0000 Subject: [PATCH 28/35] Omit empty string from Patch test parameters (as PATCH returns 500 if nothing is changed) --- src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index ce8f3a40f..b91ccbd06 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -370,7 +370,6 @@ public async Task Patch_DeliveryChannelPolicy_201() } [Theory] - [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array [InlineData("[\"\"]")] // Array containing an empty value [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data @@ -405,7 +404,6 @@ public async Task Patch_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(stri } [Theory] - [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array [InlineData("[\"\"]")] // Array containing an empty value [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data From d7c2c33d446fc4c162b6b62561a31e571906c5f5 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 16:29:29 +0000 Subject: [PATCH 29/35] Update HydraDeliveryChannelPolicyValidatorTests to test Patch and Put separately --- ...ydraDeliveryChannelPolicyValidatorTests.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs index 4db3bbb04..c4a669991 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs @@ -92,24 +92,35 @@ public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPost() } [Fact] - public void NewDeliveryChannelPolicy_CannotHave_Name_OnPutOrPatch() + public void NewDeliveryChannelPolicy_CannotHave_Name_OnPut() { var policy = new DeliveryChannelPolicy() { Name = "my-delivery-channel-policy" }; - var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put-patch")); + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put")); result.ShouldHaveValidationErrorFor(p => p.Name); } [Fact] - public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPutOrPatch() + public void NewDeliveryChannelPolicy_CannotHave_Name_OnPatch() + { + var policy = new DeliveryChannelPolicy() + { + Name = "my-delivery-channel-policy" + }; + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "patch")); + result.ShouldHaveValidationErrorFor(p => p.Name); + } + + [Fact] + public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPut() { var policy = new DeliveryChannelPolicy() { PolicyData = null, }; - var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put-patch")); + var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put")); result.ShouldHaveValidationErrorFor(p => p.PolicyData); } } \ No newline at end of file From 986b59c2887ed1f7288c3b50239114bcdc627fa4 Mon Sep 17 00:00:00 2001 From: griffri Date: Thu, 22 Feb 2024 16:32:44 +0000 Subject: [PATCH 30/35] Move name validation to HydraDeliveryChannelPolicyValidator, Add Swagger documentation, Replace IsValidDeliveryChannel() in DeliveryChannelPoliciesController to AssetDeliveryChannels.IsValidChannel(),Add DeliveryChannelPolicyDataValidator as singleton, Fix 'iif-img' typo --- ...ydraDeliveryChannelPolicyValidatorTests.cs | 2 +- .../DeliveryChannelPoliciesController.cs | 95 ++++++++++++------- .../HydraDeliveryChannelPolicyValidator.cs | 23 ++--- src/protagonist/API/Startup.cs | 2 +- .../Assets/AssetDeliveryChannels.cs | 6 ++ 5 files changed, 81 insertions(+), 47 deletions(-) diff --git a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs index c4a669991..63f37d86f 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs @@ -41,7 +41,7 @@ public void NewDeliveryChannelPolicy_CannotHave_Channel() { var policy = new DeliveryChannelPolicy() { - Channel = "iif-img" + Channel = "iiif-img" }; var result = sut.TestValidate(policy); result.ShouldHaveValidationErrorFor(p => p.Channel); diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index 74cdc456f..c8e35f3f3 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -37,6 +37,10 @@ public class DeliveryChannelPoliciesController : HydraController } + /// + /// Get a collection of nested DeliveryChannelPolicy collections, sorted by channel + /// + /// HydraCollection of DeliveryChannelPolicy HydraCollection [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetDeliveryChannelPolicyCollections( @@ -75,6 +79,10 @@ public class DeliveryChannelPoliciesController : HydraController return new OkObjectResult(result); } + /// + /// Get a collection of the customer's delivery channel policies for a specific channel + /// + /// HydraCollection of DeliveryChannelPolicy [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [Route("{deliveryChannelName}")] @@ -83,7 +91,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromRoute] string deliveryChannelName, CancellationToken cancellationToken) { - if (!IsValidDeliveryChannel(deliveryChannelName)) + if (!AssetDeliveryChannels.IsValidChannel(deliveryChannelName)) { return this.HydraProblem($"'{deliveryChannelName}' is not a valid delivery channel", null, 400, "Invalid delivery channel"); @@ -99,6 +107,19 @@ public class DeliveryChannelPoliciesController : HydraController ); } + /// + /// Create a new policy for a specified delivery channel + /// + /// + /// Sample request: + /// + /// POST: /customers/1/deliveryChannelPolicies/iiif-av + /// { + /// "name": "my-video-policy" + /// "displayName": "My Video Policy", + /// "policyData": "["video-mp4-720p"]" + /// } + /// [HttpPost] [Route("{deliveryChannelName}")] [ProducesResponseType(StatusCodes.Status201Created)] @@ -116,12 +137,6 @@ public class DeliveryChannelPoliciesController : HydraController 400, "Invalid delivery channel policy"); } - if (!IsValidName(hydraDeliveryChannelPolicy.Name)) - { - return this.HydraProblem($"'The name specified for this delivery channel policy is invalid", null, - 400, "Invalid delivery channel policy"); - } - var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "post"), cancellationToken); if (!validationResult.IsValid) @@ -139,6 +154,10 @@ public class DeliveryChannelPoliciesController : HydraController cancellationToken: cancellationToken); } + /// + /// Get a delivery channel policy belonging to a customer + /// + /// DeliveryChannelPolicy [HttpGet] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -159,6 +178,19 @@ public class DeliveryChannelPoliciesController : HydraController cancellationToken: cancellationToken); } + /// + /// Create or update a specified customer delivery channel policy - "name" must be specified in URI + /// + /// + /// Sample request: + /// + /// PUT: /customers/1/deliveryChannelPolicies/iiif-av/my-video-policy + /// { + /// "displayName": "My Updated Video Policy", + /// "policyData": "["video-mp4-720p"]" + /// } + /// + /// DeliveryChannelPolicy [HttpPut] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -177,13 +209,10 @@ public class DeliveryChannelPoliciesController : HydraController return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, 400, "Invalid delivery channel policy"); } - - if (!IsValidName(deliveryChannelPolicyName)) - { - return this.HydraProblem($"'The name specified for this delivery channel policy is invalid", null, - 400, "Invalid delivery channel policy"); - } - + + hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; + var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "put"), cancellationToken); if (!validationResult.IsValid) @@ -192,8 +221,6 @@ public class DeliveryChannelPoliciesController : HydraController } hydraDeliveryChannelPolicy.CustomerId = customerId; - hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; var updateDeliveryChannelPolicy = new UpdateDeliveryChannelPolicy(customerId, hydraDeliveryChannelPolicy.ToDlcsModel()); @@ -204,6 +231,18 @@ public class DeliveryChannelPoliciesController : HydraController cancellationToken: cancellationToken); } + /// + /// Update the supplied fields for a specified customer delivery channel policy + /// + /// + /// Sample request: + /// + /// PATCH: /customers/1/deliveryChannelPolicies/iiif-av/my-video-policy + /// { + /// "displayName": "My Updated Video Policy" + /// } + /// + /// DeliveryChannelPolicy [HttpPatch] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -223,11 +262,8 @@ public class DeliveryChannelPoliciesController : HydraController 400, "Invalid delivery channel policy"); } - if (!IsValidName(deliveryChannelPolicyName)) - { - return this.HydraProblem($"The name specified for this delivery channel policy is invalid", null, - 400, "Invalid delivery channel policy"); - } + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; + hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "patch"), cancellationToken); @@ -237,8 +273,6 @@ public class DeliveryChannelPoliciesController : HydraController } hydraDeliveryChannelPolicy.CustomerId = customerId; - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; - hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; var patchDeliveryChannelPolicy = new PatchDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName) { @@ -252,6 +286,10 @@ public class DeliveryChannelPoliciesController : HydraController cancellationToken: cancellationToken); } + + /// + /// Delete a specified delivery channel policy + /// [HttpDelete] [Route("{deliveryChannelName}/{deliveryChannelPolicyName}")] [ProducesResponseType(StatusCodes.Status202Accepted)] @@ -273,19 +311,8 @@ public class DeliveryChannelPoliciesController : HydraController return await HandleDelete(deleteDeliveryChannelPolicy); } - private bool IsValidDeliveryChannel(string deliveryChannelPolicyName) - { - return AssetDeliveryChannels.All.Contains(deliveryChannelPolicyName); - } - private bool IsPermittedDeliveryChannel(string deliveryChannelPolicyName) { return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); } - - private bool IsValidName(string? inputName) - { - const string regex = "[\\sA-Z]"; // Delivery channel policy names should not contain capital letters or spaces - return !(string.IsNullOrEmpty(inputName) || Regex.IsMatch(inputName, regex)); - } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs index bec7fa42b..44879e7c5 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -21,6 +21,10 @@ public HydraDeliveryChannelPolicyValidator() RuleFor(p => p.Name) .NotEmpty() .WithMessage("'name' is required"); + RuleFor(p => p.Name) + .Must(IsValidName!) + .When(p => !string.IsNullOrEmpty(p.Name)) + .WithMessage("'name' is invalid"); RuleFor(p => p.PolicyData) .NotEmpty() .WithMessage("'policyData' is required"); @@ -28,24 +32,21 @@ public HydraDeliveryChannelPolicyValidator() RuleSet("put", () => { RuleFor(p => p.Name) - .Empty() - .WithMessage("'name' should be set in the URL"); + .Must(IsValidName!) + .WithMessage("'name' is invalid"); RuleFor(p => p.PolicyData) .NotEmpty() .WithMessage("'policyData' is required"); }); - RuleSet("patch", () => - { - RuleFor(p => p.Name) - .Empty() - .WithMessage("'name' should be set in the URL"); - }); - RuleFor(p => p.Channel) - .Empty() - .WithMessage("'channel' should be set in the URL"); RuleFor(p => p.Modified) .Empty().WithMessage(c => $"'policyModified' is generated by the DLCS and cannot be set manually"); RuleFor(p => p.Created) .Empty().WithMessage(c => $"'policyCreated' is generated by the DLCS and cannot be set manually"); } + + private bool IsValidName(string inputName) + { + const string regex = "[\\sA-Z]"; // Delivery channel policy names should not contain capital letters or spaces + return !(Regex.IsMatch(inputName, regex)); + } } \ No newline at end of file diff --git a/src/protagonist/API/Startup.cs b/src/protagonist/API/Startup.cs index d34c72c0d..55316208b 100644 --- a/src/protagonist/API/Startup.cs +++ b/src/protagonist/API/Startup.cs @@ -75,7 +75,7 @@ public void ConfigureServices(IServiceCollection services) .AddSingleton() .AddScoped() .AddTransient() - .AddTransient() + .AddSingleton() .AddValidatorsFromAssemblyContaining() .ConfigureMediatR() .AddNamedQueriesCore() diff --git a/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs b/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs index ba4639415..8cc6d1d74 100644 --- a/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs +++ b/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs @@ -42,4 +42,10 @@ public static bool HasDeliveryChannel(this Asset asset, string deliveryChannel) /// public static bool HasSingleDeliveryChannel(this Asset asset, string deliveryChannel) => asset.DeliveryChannels.ContainsOnly(deliveryChannel); + + /// + /// Checks if string is a valid delivery channel + /// + public static bool IsValidChannel(string deliveryChannel) => + All.Contains(deliveryChannel); } \ No newline at end of file From a426582f8e7102956a61fd25388344d147222853 Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 23 Feb 2024 08:12:19 +0000 Subject: [PATCH 31/35] Fix issues raised in review: Remove implicit conversion of customer Ids in database fixture Shorten ValidateTimeBasedPolicyData checks Remove redundant setters Change DELETE delivery channel policy to only allow delivery channel policies owned by the customer to be deleted Return false instead of setting isInvalid to true when parsing size parameter Allow iiif-av policyData to contain multiple transcode policies, update tests --- ...DeliveryChannelPolicyDataValidatorTests.cs | 13 ++++---- ...ydraDeliveryChannelPolicyValidatorTests.cs | 33 ------------------- .../Integration/DeliveryChannelTests.cs | 6 ++-- .../Requests/DeleteDeliveryChannelPolicy.cs | 5 +-- .../Requests/GetDeliveryChannelPolicies.cs | 2 +- .../Requests/GetDeliveryChannelPolicy.cs | 4 +-- .../Requests/PatchDeliveryChannelPolicy.cs | 4 +-- .../DeliveryChannelPolicyDataValidator.cs | 24 +++++--------- .../HydraDeliveryChannelPolicyValidator.cs | 2 +- .../Integration/DlcsDatabaseFixture.cs | 2 +- 10 files changed, 28 insertions(+), 67 deletions(-) diff --git a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs index 5990e5e1a..cf866ba1b 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidatorTests.cs @@ -69,8 +69,8 @@ public void PolicyDataValidator_ReturnsFalse_ForEmptyThumbSizes(string policyDat public void PolicyDataValidator_ReturnsTrue_ForValidAvPolicy() { // Arrange - var policyData = "[\"media-format-quality\"]"; // For now, any single string value is accepted - this will need - // to be rewritten once the API requires a valid transcoder policy + var policyData = "[\"media-format-quality\"]"; // For now, any single string values are accepted - this will need + // to be rewritten once the API requires policies that exist // Act var result = sut.Validate(policyData, "iiif-av"); @@ -79,12 +79,11 @@ public void PolicyDataValidator_ReturnsTrue_ForValidAvPolicy() result.Should().BeTrue(); } - [Fact] - public void PolicyDataValidator_ReturnsFalse_ForBadAvPolicy() + [Theory] + [InlineData("[\"\"]")] + [InlineData("[\"policy-1\",\"\"]")] + public void PolicyDataValidator_ReturnsFalse_ForBadAvPolicy(string policyData) { - // Arrange - var policyData = "[\"policy-1\",\"policy-2\"]"; - // Arrange and Act var result = sut.Validate(policyData, "iiif-av"); diff --git a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs index 63f37d86f..199bafe8c 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs @@ -36,17 +36,6 @@ public void NewDeliveryChannelPolicy_CannotHave_CustomerId() result.ShouldHaveValidationErrorFor(p => p.CustomerId); } - [Fact] - public void NewDeliveryChannelPolicy_CannotHave_Channel() - { - var policy = new DeliveryChannelPolicy() - { - Channel = "iiif-img" - }; - var result = sut.TestValidate(policy); - result.ShouldHaveValidationErrorFor(p => p.Channel); - } - [Fact] public void NewDeliveryChannelPolicy_CannotHave_PolicyCreated() { @@ -91,28 +80,6 @@ public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPost() result.ShouldHaveValidationErrorFor(p => p.PolicyData); } - [Fact] - public void NewDeliveryChannelPolicy_CannotHave_Name_OnPut() - { - var policy = new DeliveryChannelPolicy() - { - Name = "my-delivery-channel-policy" - }; - var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "put")); - result.ShouldHaveValidationErrorFor(p => p.Name); - } - - [Fact] - public void NewDeliveryChannelPolicy_CannotHave_Name_OnPatch() - { - var policy = new DeliveryChannelPolicy() - { - Name = "my-delivery-channel-policy" - }; - var result = sut.TestValidate(policy, p => p.IncludeRuleSets("default", "patch")); - result.ShouldHaveValidationErrorFor(p => p.Name); - } - [Fact] public void NewDeliveryChannelPolicy_Requires_PolicyData_OnPut() { diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index b91ccbd06..db9978f01 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -159,7 +159,7 @@ public async Task Post_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(strin [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array [InlineData("[\"\"]")] // Array containing an empty value - [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""transcode-policy-1\"",\""\""]")] // Invalid data [InlineData(@"[\""transcode-policy\""")] // Invalid JSON public async Task Post_DeliveryChannelPolicy_400_IfAvPolicyDataInvalid(string policyData) { @@ -299,7 +299,7 @@ public async Task Put_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(string [InlineData("")] // No PolicyData specified [InlineData("[]")] // Empty array [InlineData("[\"\"]")] // Array containing an empty value - [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""transcode-policy-1\"",\""\""]")] // Invalid data [InlineData(@"[\""transcode-policy\""")] // Invalid JSON public async Task Put_DeliveryChannelPolicy_400_IfAvPolicyDataInvalid(string policyData) { @@ -406,7 +406,7 @@ public async Task Patch_DeliveryChannelPolicy_400_IfThumbsPolicyDataInvalid(stri [Theory] [InlineData("[]")] // Empty array [InlineData("[\"\"]")] // Array containing an empty value - [InlineData(@"[\""foo\"",\""bar\""]")] // Invalid data + [InlineData(@"[\""transcode-policy-1\"",\""\""]")] // Invalid data [InlineData(@"[\""transcode-policy\""")] // Invalid JSON public async Task Patch_DeliveryChannelPolicy_400_IfAvPolicyDataInvalid(string policyData) { diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs index ba46db874..0e1e44d52 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/DeleteDeliveryChannelPolicy.cs @@ -9,8 +9,8 @@ namespace API.Features.DeliveryChannels.Requests; public class DeleteDeliveryChannelPolicy: IRequest> { public int CustomerId { get; } - public string DeliveryChannelName { get; set; } - public string DeliveryChannelPolicyName { get; set; } + public string DeliveryChannelName { get; } + public string DeliveryChannelPolicyName { get; } public DeleteDeliveryChannelPolicy(int customerId, string deliveryChannelName, string deliveryChannelPolicyName) { @@ -32,6 +32,7 @@ public DeleteDeliveryChannelPolicyHandler(DlcsContext dbContext) public async Task> Handle(DeleteDeliveryChannelPolicy request, CancellationToken cancellationToken) { var policy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => + p.Customer == request.CustomerId && p.Name == request.DeliveryChannelPolicyName && p.Channel == request.DeliveryChannelName, cancellationToken); diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs index acef5ed80..88211b8bf 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs @@ -10,7 +10,7 @@ namespace API.Features.DeliveryChannels.Requests; public class GetDeliveryChannelPolicies: IRequest>> { public int CustomerId { get; } - public string DeliveryChannelName { get; set; } + public string DeliveryChannelName { get; } public GetDeliveryChannelPolicies(int customerId, string deliveryChannelName) { diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs index 929ffc7d5..98532bc9c 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicy.cs @@ -9,8 +9,8 @@ namespace API.Features.DeliveryChannels.Requests; public class GetDeliveryChannelPolicy: IRequest> { public int CustomerId { get; } - public string DeliveryChannelName { get; set; } - public string DeliveryChannelPolicyName { get; set; } + public string DeliveryChannelName { get; } + public string DeliveryChannelPolicyName { get; } public GetDeliveryChannelPolicy(int customerId, string deliveryChannelName, string deliveryChannelPolicyName) { diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs index 4b815aa1a..eb5df7aea 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs @@ -13,9 +13,9 @@ public class PatchDeliveryChannelPolicy : IRequest p.Name) .Must(IsValidName!) - .When(p => !string.IsNullOrEmpty(p.Name)) .WithMessage("'name' is invalid"); RuleFor(p => p.PolicyData) .NotEmpty() @@ -47,6 +46,7 @@ public HydraDeliveryChannelPolicyValidator() private bool IsValidName(string inputName) { const string regex = "[\\sA-Z]"; // Delivery channel policy names should not contain capital letters or spaces + if (string.IsNullOrEmpty(inputName)) return false; return !(Regex.IsMatch(inputName, regex)); } } \ No newline at end of file diff --git a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs index 56d7aed16..b27c16096 100644 --- a/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs +++ b/src/protagonist/Test.Helpers/Integration/DlcsDatabaseFixture.cs @@ -64,7 +64,7 @@ public void CleanUp() DbContext.Database.ExecuteSqlRaw("DELETE FROM \"EntityCounters\" WHERE \"Type\" = 'space' AND \"Customer\" != 99"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"EntityCounters\" WHERE \"Type\" = 'space-images' AND \"Customer\" != 99"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"EntityCounters\" WHERE \"Type\" = 'customer-images' AND \"Scope\" != '99'"); - DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DeliveryChannelPolicies\" WHERE \"Customer\" not in ('1','99')"); + DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DeliveryChannelPolicies\" WHERE \"Customer\" not in (1,99)"); DbContext.Database.ExecuteSqlRaw("DELETE FROM \"DefaultDeliveryChannels\" WHERE \"Customer\" <> 1"); DbContext.ChangeTracker.Clear(); } From b0e9ef676782cbe4619412b279eabe75a44f154b Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 23 Feb 2024 11:17:24 +0000 Subject: [PATCH 32/35] Add mediatr request for getting delivery channel policy collections, remove redundant exception definition from DeliveryChannelPolicyDataValidator --- .../DeliveryChannelPoliciesController.cs | 36 ++--------- .../GetDeliveryChannelPolicyCollections.cs | 62 +++++++++++++++++++ .../DeliveryChannelPolicyDataValidator.cs | 2 +- 3 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index c8e35f3f3..24a9eedeb 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -44,39 +44,13 @@ public class DeliveryChannelPoliciesController : HydraController [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetDeliveryChannelPolicyCollections( - [FromRoute] int customerId) + [FromRoute] int customerId, + CancellationToken cancellationToken) { - var baseUrl = Request.GetDisplayUrl(Request.Path); - - var hydraPolicyCollections = new List>() - { - new(baseUrl, AssetDeliveryChannels.Image) - { - Title = "Policies for IIIF Image service delivery", - }, - new(baseUrl, AssetDeliveryChannels.Thumbnails) - { - Title = "Policies for thumbnails as IIIF Image Services", - }, - new(baseUrl, AssetDeliveryChannels.Timebased) - { - Title = "Policies for Audio and Video delivery", - }, - new(baseUrl, AssetDeliveryChannels.File) - { - Title = "Policies for File delivery", - } - }; - - var result = new HydraCollection>() - { - WithContext = true, - Members = hydraPolicyCollections.ToArray(), - TotalItems = hydraPolicyCollections.Count, - Id = Request.GetJsonLdId() - }; + var request = new GetDeliveryChannelPolicyCollections(customerId, Request.GetDisplayUrl(Request.Path), Request.GetJsonLdId()); + var result = await Mediator.Send(request, cancellationToken); - return new OkObjectResult(result); + return Ok(result); } /// diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs new file mode 100644 index 000000000..772f595bc --- /dev/null +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs @@ -0,0 +1,62 @@ +using DLCS.Model.Assets; +using DLCS.Model.Policies; +using DLCS.Repository; +using Hydra.Collections; +using MediatR; + +namespace API.Features.DeliveryChannels.Requests; + +public class GetDeliveryChannelPolicyCollections: IRequest>> +{ + public int CustomerId { get; } + public string BaseUrl { get; } + public string JsonLdId { get; } + + public GetDeliveryChannelPolicyCollections(int customerId, string baseUrl, string jsonLdId) + { + CustomerId = customerId; + BaseUrl = baseUrl; + JsonLdId = jsonLdId; + } +} + +public class GetDeliveryChannelPolicyCollectionsHandler : IRequestHandler>> +{ + private readonly DlcsContext dbContext; + + public GetDeliveryChannelPolicyCollectionsHandler(DlcsContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task>> Handle(GetDeliveryChannelPolicyCollections request, CancellationToken cancellationToken) + { + var policyCollections = new HydraNestedCollection[] + { + new(request.BaseUrl, AssetDeliveryChannels.Image) + { + Title = "Policies for IIIF Image service delivery", + }, + new(request.BaseUrl, AssetDeliveryChannels.Thumbnails) + { + Title = "Policies for thumbnails as IIIF Image Services", + }, + new(request.BaseUrl, AssetDeliveryChannels.Timebased) + { + Title = "Policies for Audio and Video delivery", + }, + new(request.BaseUrl, AssetDeliveryChannels.File) + { + Title = "Policies for File delivery", + } + }; + + return new HydraCollection>() + { + WithContext = true, + Members = policyCollections, + TotalItems = policyCollections.Length, + Id = request.JsonLdId, + }; + } +} \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs index a13892c81..822aa09a5 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/DeliveryChannelPolicyDataValidator.cs @@ -24,7 +24,7 @@ public bool Validate(string policyDataJson, string channel) { policyData = JsonSerializer.Deserialize(policyDataJson); } - catch(JsonException ex) + catch(JsonException) { return null; } From 0882372fda11a94cf9d18a48a319d821e0f3893d Mon Sep 17 00:00:00 2001 From: griffri Date: Fri, 23 Feb 2024 13:55:35 +0000 Subject: [PATCH 33/35] Fix more issues raised in review Rename GetDeliveryChannelPolicies to GetPoliciesFromDeliveryChannel Remove whitespace in AssetDeliveryChannels Add test for ensuring that attempting to POST a delivery channel with a name that's taken returns a 409 Include displayName and policyData in PatchDeliveryChannelPolicy request constructor Only attempt to patch a delivery channel policy if a field has been changed Allow empty strings to be specified in delivery channel policy PATCH Move policy data validation into HydraDeliveryChannelPolicyValidator Handle channel validation logic in HydraDeliveryChannelPolicyValidator --- ...ydraDeliveryChannelPolicyValidatorTests.cs | 2 +- .../Integration/DeliveryChannelTests.cs | 30 +++++++++++ .../DeliveryChannelPoliciesController.cs | 50 +++---------------- .../Requests/CreateDeliveryChannelPolicy.cs | 9 ---- ...s.cs => GetPoliciesFromDeliveryChannel.cs} | 8 +-- .../Requests/PatchDeliveryChannelPolicy.cs | 29 ++++------- .../Requests/UpdateDeliveryChannelPolicy.cs | 9 ---- .../HydraDeliveryChannelPolicyValidator.cs | 35 ++++++++++++- .../Assets/AssetDeliveryChannels.cs | 1 - 9 files changed, 86 insertions(+), 87 deletions(-) rename src/protagonist/API/Features/DeliveryChannels/Requests/{GetDeliveryChannelPolicies.cs => GetPoliciesFromDeliveryChannel.cs} (73%) diff --git a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs index 199bafe8c..d17586068 100644 --- a/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs +++ b/src/protagonist/API.Tests/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidatorTests.cs @@ -11,7 +11,7 @@ public class HydraDeliveryChannelPolicyValidatorTests public HydraDeliveryChannelPolicyValidatorTests() { - sut = new HydraDeliveryChannelPolicyValidator(); + sut = new HydraDeliveryChannelPolicyValidator(new DeliveryChannelPolicyDataValidator()); } [Fact] diff --git a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs index db9978f01..0ed04963b 100644 --- a/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs +++ b/src/protagonist/API.Tests/Integration/DeliveryChannelTests.cs @@ -108,6 +108,36 @@ public async Task Post_DeliveryChannelPolicy_400_IfChannelInvalid() response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } + [Fact] + public async Task Post_DeliveryChannelPolicy_409_IfNameTaken() + { + // Arrange + const int customerId = 88; + const string newDeliveryChannelPolicyJson = @"{ + ""name"": ""post-existing-policy"", + ""policyData"": ""[\""100,100\""]"" + }"; + + var path = $"customers/{customerId}/deliveryChannelPolicies/thumbs"; + var policy = new DLCS.Model.Policies.DeliveryChannelPolicy() + { + Customer = customerId, + Name = "post-existing-policy", + Channel = "thumbs", + PolicyData = "[\"100,100\"]" + }; + + await dbContext.DeliveryChannelPolicies.AddAsync(policy); + await dbContext.SaveChangesAsync(); + + // Act + var content = new StringContent(newDeliveryChannelPolicyJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(path, content); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + [Fact] public async Task Post_DeliveryChannelPolicy_400_IfNameInvalid() { diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index 24a9eedeb..1f48b4a15 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -24,12 +24,6 @@ namespace API.Features.DeliveryChannels; [ApiController] public class DeliveryChannelPoliciesController : HydraController { - private readonly string[] allowedDeliveryChannels = - { - AssetDeliveryChannels.Thumbnails, - AssetDeliveryChannels.Timebased, - }; - public DeliveryChannelPoliciesController( IMediator mediator, IOptions options) : base(options.Value, mediator) @@ -71,9 +65,9 @@ public class DeliveryChannelPoliciesController : HydraController 400, "Invalid delivery channel"); } - var request = new GetDeliveryChannelPolicies(customerId, deliveryChannelName); + var request = new GetPoliciesFromDeliveryChannel(customerId, deliveryChannelName); - return await HandleListFetch( + return await HandleListFetch( request, p => p.ToHydra(GetUrlRoots().BaseUrl), errorTitle: "Failed to get delivery channel policies", @@ -105,11 +99,7 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsPermittedDeliveryChannel(deliveryChannelName)) - { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, - 400, "Invalid delivery channel policy"); - } + hydraDeliveryChannelPolicy.Channel = deliveryChannelName; var validationResult = await validator.ValidateAsync(hydraDeliveryChannelPolicy, policy => policy.IncludeRuleSets("default", "post"), cancellationToken); @@ -119,7 +109,6 @@ public class DeliveryChannelPoliciesController : HydraController } hydraDeliveryChannelPolicy.CustomerId = customerId; - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; var request = new CreateDeliveryChannelPolicy(customerId, hydraDeliveryChannelPolicy.ToDlcsModel()); return await HandleUpsert(request, @@ -178,12 +167,6 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsPermittedDeliveryChannel(deliveryChannelName)) - { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, - 400, "Invalid delivery channel policy"); - } - hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; hydraDeliveryChannelPolicy.Channel = deliveryChannelName; @@ -230,12 +213,6 @@ public class DeliveryChannelPoliciesController : HydraController [FromServices] HydraDeliveryChannelPolicyValidator validator, CancellationToken cancellationToken) { - if(!IsPermittedDeliveryChannel(deliveryChannelName)) - { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, - 400, "Invalid delivery channel policy"); - } - hydraDeliveryChannelPolicy.Channel = deliveryChannelName; hydraDeliveryChannelPolicy.Name = deliveryChannelPolicyName; @@ -247,12 +224,10 @@ public class DeliveryChannelPoliciesController : HydraController } hydraDeliveryChannelPolicy.CustomerId = customerId; - - var patchDeliveryChannelPolicy = new PatchDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName) - { - DisplayName = hydraDeliveryChannelPolicy.DisplayName, - PolicyData = hydraDeliveryChannelPolicy.PolicyData - }; + + var patchDeliveryChannelPolicy = + new PatchDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName, + hydraDeliveryChannelPolicy.DisplayName, hydraDeliveryChannelPolicy.PolicyData); return await HandleUpsert(patchDeliveryChannelPolicy, s => s.ToHydra(GetUrlRoots().BaseUrl), @@ -273,20 +248,9 @@ public class DeliveryChannelPoliciesController : HydraController [FromRoute] string deliveryChannelName, [FromRoute] string deliveryChannelPolicyName) { - if(!IsPermittedDeliveryChannel(deliveryChannelName)) - { - return this.HydraProblem($"'{deliveryChannelName}' is not a valid/permitted delivery channel", null, - 400, "Invalid delivery channel policy"); - } - var deleteDeliveryChannelPolicy = new DeleteDeliveryChannelPolicy(customerId, deliveryChannelName, deliveryChannelPolicyName); return await HandleDelete(deleteDeliveryChannelPolicy); } - - private bool IsPermittedDeliveryChannel(string deliveryChannelPolicyName) - { - return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); - } } \ No newline at end of file diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs index 61a212345..914c17514 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs @@ -24,12 +24,10 @@ public CreateDeliveryChannelPolicy(int customerId, DeliveryChannelPolicy deliver public class CreateDeliveryChannelPolicyHandler : IRequestHandler> { private readonly DlcsContext dbContext; - private readonly DeliveryChannelPolicyDataValidator policyDataValidator; public CreateDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelPolicyDataValidator policyDataValidator) { this.dbContext = dbContext; - this.policyDataValidator = policyDataValidator; } public async Task> Handle(CreateDeliveryChannelPolicy request, CancellationToken cancellationToken) @@ -46,13 +44,6 @@ public async Task> Handle(CreateDelive $"A {request.DeliveryChannelPolicy.Channel}' policy called '{request.DeliveryChannelPolicy.Name}' already exists" , WriteResult.Conflict); } - - if(!policyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) - { - return ModifyEntityResult.Failure( - $"'policyData' contains bad JSON or invalid data", - WriteResult.FailedValidation); - } var newDeliveryChannelPolicy = new DeliveryChannelPolicy() { diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesFromDeliveryChannel.cs similarity index 73% rename from src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesFromDeliveryChannel.cs index 88211b8bf..32ed41897 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicies.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesFromDeliveryChannel.cs @@ -7,19 +7,19 @@ namespace API.Features.DeliveryChannels.Requests; -public class GetDeliveryChannelPolicies: IRequest>> +public class GetPoliciesFromDeliveryChannel: IRequest>> { public int CustomerId { get; } public string DeliveryChannelName { get; } - public GetDeliveryChannelPolicies(int customerId, string deliveryChannelName) + public GetPoliciesFromDeliveryChannel(int customerId, string deliveryChannelName) { CustomerId = customerId; DeliveryChannelName = deliveryChannelName; } } -public class GetDeliveryChannelPoliciesHandler : IRequestHandler>> +public class GetDeliveryChannelPoliciesHandler : IRequestHandler>> { private readonly DlcsContext dbContext; @@ -28,7 +28,7 @@ public GetDeliveryChannelPoliciesHandler(DlcsContext dbContext) this.dbContext = dbContext; } - public async Task>> Handle(GetDeliveryChannelPolicies request, CancellationToken cancellationToken) + public async Task>> Handle(GetPoliciesFromDeliveryChannel request, CancellationToken cancellationToken) { var deliveryChannelPolicies = await dbContext.DeliveryChannelPolicies .AsNoTracking() diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs index eb5df7aea..4d27d7978 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs @@ -21,23 +21,23 @@ public class PatchDeliveryChannelPolicy : IRequest> { private readonly DlcsContext dbContext; - private readonly DeliveryChannelPolicyDataValidator policyDataValidator; public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelPolicyDataValidator policyDataValidator) { this.dbContext = dbContext; - this.policyDataValidator = policyDataValidator; } public async Task> Handle(PatchDeliveryChannelPolicy request, @@ -58,35 +58,26 @@ public PatchDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelP var hasBeenChanged = false; - if (request.DisplayName.HasText()) + if (request.DisplayName != null) { existingDeliveryChannelPolicy.DisplayName = request.DisplayName; hasBeenChanged = true; } - if (request.PolicyData.HasText()) + if (request.PolicyData != null) { existingDeliveryChannelPolicy.PolicyData = request.PolicyData; - - if(!policyDataValidator.Validate(request.PolicyData, existingDeliveryChannelPolicy.Channel)) - { - return ModifyEntityResult.Failure( - $"'policyData' contains bad JSON or invalid data", - WriteResult.FailedValidation); - } - hasBeenChanged = true; } if (hasBeenChanged) { existingDeliveryChannelPolicy.Modified = DateTime.UtcNow; - } - - var rowCount = await dbContext.SaveChangesAsync(cancellationToken); - if (rowCount == 0) - { - return ModifyEntityResult.Failure("Unable to patch delivery channel policy", WriteResult.Error); + var rowCount = await dbContext.SaveChangesAsync(cancellationToken); + if (rowCount == 0) + { + return ModifyEntityResult.Failure("Unable to patch delivery channel policy", WriteResult.Error); + } } return ModifyEntityResult.Success(existingDeliveryChannelPolicy); diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs index 00e99f90a..93deb3bd4 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/UpdateDeliveryChannelPolicy.cs @@ -24,23 +24,14 @@ public UpdateDeliveryChannelPolicy(int customerId, DeliveryChannelPolicy deliver public class UpdateDeliveryChannelPolicyHandler : IRequestHandler> { private readonly DlcsContext dbContext; - private readonly DeliveryChannelPolicyDataValidator policyDataValidator; public UpdateDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannelPolicyDataValidator policyDataValidator) { this.dbContext = dbContext; - this.policyDataValidator = policyDataValidator; } public async Task> Handle(UpdateDeliveryChannelPolicy request, CancellationToken cancellationToken) { - if(!policyDataValidator.Validate(request.DeliveryChannelPolicy.PolicyData, request.DeliveryChannelPolicy.Channel)) - { - return ModifyEntityResult.Failure( - $"'policyData' contains bad JSON or invalid data", - WriteResult.FailedValidation); - } - var existingDeliveryChannelPolicy = await dbContext.DeliveryChannelPolicies.SingleOrDefaultAsync(p => p.Customer == request.CustomerId && p.Channel == request.DeliveryChannelPolicy.Channel && diff --git a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs index 66c50cf75..4dd1793ca 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Validation/HydraDeliveryChannelPolicyValidator.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using DLCS.Model.Assets; using FluentValidation; namespace API.Features.DeliveryChannels.Validation; @@ -8,14 +9,31 @@ namespace API.Features.DeliveryChannels.Validation; /// public class HydraDeliveryChannelPolicyValidator : AbstractValidator { - public HydraDeliveryChannelPolicyValidator() + private readonly DeliveryChannelPolicyDataValidator policyDataValidator; + + private readonly string[] allowedDeliveryChannels = + { + AssetDeliveryChannels.Thumbnails, + AssetDeliveryChannels.Timebased, + }; + + public HydraDeliveryChannelPolicyValidator(DeliveryChannelPolicyDataValidator policyDataValidator) { + this.policyDataValidator = policyDataValidator; + RuleFor(p => p.Id) .Empty() .WithMessage(p => $"DLCS must allocate named delivery channel policy id, but id {p.Id} was supplied"); RuleFor(p => p.CustomerId) .Empty() .WithMessage("Should not include user id"); + RuleFor(p => p.Channel) + .NotEmpty() + .WithMessage("'channel' is required"); + RuleFor(p => p.Channel) + .Must(IsPermittedDeliveryChannel!) + .When(p => !string.IsNullOrEmpty(p.Channel)) + .WithMessage(p => $"'{p.Channel}' is not a valid/permitted delivery channel"); RuleSet("post", () => { RuleFor(p => p.Name) @@ -37,6 +55,10 @@ public HydraDeliveryChannelPolicyValidator() .NotEmpty() .WithMessage("'policyData' is required"); }); + RuleFor(p => p.PolicyData) + .Must((p, pd) => IsValidPolicyData(pd, p.Channel)) + .When(p => !string.IsNullOrEmpty(p.PolicyData)) + .WithMessage(p => $"'policyData' contains bad JSON or invalid data"); RuleFor(p => p.Modified) .Empty().WithMessage(c => $"'policyModified' is generated by the DLCS and cannot be set manually"); RuleFor(p => p.Created) @@ -49,4 +71,15 @@ private bool IsValidName(string inputName) if (string.IsNullOrEmpty(inputName)) return false; return !(Regex.IsMatch(inputName, regex)); } + + private bool IsPermittedDeliveryChannel(string deliveryChannelPolicyName) + { + return allowedDeliveryChannels.Contains(deliveryChannelPolicyName); + } + + private bool IsValidPolicyData(string? policyData, string? channel) + { + if (string.IsNullOrEmpty(policyData) || string.IsNullOrEmpty(channel)) return false; + return policyDataValidator.Validate(policyData, channel); + } } \ No newline at end of file diff --git a/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs b/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs index 8cc6d1d74..c2f86a817 100644 --- a/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs +++ b/src/protagonist/DLCS.Model/Assets/AssetDeliveryChannels.cs @@ -6,7 +6,6 @@ namespace DLCS.Model.Assets; public static class AssetDeliveryChannels { - public const string Image = "iiif-img"; public const string Thumbnails = "thumbs"; public const string Timebased = "iiif-av"; From a60fe8f300d483fad6e364afca6c2fb18952bcde Mon Sep 17 00:00:00 2001 From: griffri Date: Tue, 27 Feb 2024 11:48:34 +0000 Subject: [PATCH 34/35] Address issues raised in review Add DbUpdateException extensions, use GetDatabaseError() to determine conflict when creating new delivery channel policies Remove redundant setters Rename GetPoliciesFromDeliveryChannel to GetPoliciesForDeliveryChannel --- .../DeliveryChannelPoliciesController.cs | 4 +- .../Requests/CreateDeliveryChannelPolicy.cs | 27 +++++------ ...el.cs => GetPoliciesForDeliveryChannel.cs} | 8 ++-- .../Requests/PatchDeliveryChannelPolicy.cs | 4 +- .../DLCS.Repository/Exceptions/DbError.cs | 13 +++++ .../Exceptions/DbUniqueConstraintError.cs | 48 +++++++++++++++++++ .../Exceptions/DbUpdateExceptionX.cs | 38 +++++++++++++++ 7 files changed, 119 insertions(+), 23 deletions(-) rename src/protagonist/API/Features/DeliveryChannels/Requests/{GetPoliciesFromDeliveryChannel.cs => GetPoliciesForDeliveryChannel.cs} (73%) create mode 100644 src/protagonist/DLCS.Repository/Exceptions/DbError.cs create mode 100644 src/protagonist/DLCS.Repository/Exceptions/DbUniqueConstraintError.cs create mode 100644 src/protagonist/DLCS.Repository/Exceptions/DbUpdateExceptionX.cs diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index 1f48b4a15..bc40663c9 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -65,9 +65,9 @@ public class DeliveryChannelPoliciesController : HydraController 400, "Invalid delivery channel"); } - var request = new GetPoliciesFromDeliveryChannel(customerId, deliveryChannelName); + var request = new GetPoliciesForDeliveryChannel(customerId, deliveryChannelName); - return await HandleListFetch( + return await HandleListFetch( request, p => p.ToHydra(GetUrlRoots().BaseUrl), errorTitle: "Failed to get delivery channel policies", diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs index 914c17514..2004a9de1 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/CreateDeliveryChannelPolicy.cs @@ -3,6 +3,7 @@ using DLCS.Core; using DLCS.Model.Policies; using DLCS.Repository; +using DLCS.Repository.Exceptions; using MediatR; using Microsoft.EntityFrameworkCore; @@ -32,19 +33,6 @@ public CreateDeliveryChannelPolicyHandler(DlcsContext dbContext, DeliveryChannel public async Task> Handle(CreateDeliveryChannelPolicy request, CancellationToken cancellationToken) { - var nameInUse = await dbContext.DeliveryChannelPolicies.AnyAsync(p => - p.Customer == request.CustomerId && - p.Channel == request.DeliveryChannelPolicy.Channel && - p.Name == request.DeliveryChannelPolicy.Name, - cancellationToken); - - if (nameInUse) - { - return ModifyEntityResult.Failure( - $"A {request.DeliveryChannelPolicy.Channel}' policy called '{request.DeliveryChannelPolicy.Name}' already exists" , - WriteResult.Conflict); - } - var newDeliveryChannelPolicy = new DeliveryChannelPolicy() { Customer = request.CustomerId, @@ -58,8 +46,17 @@ public async Task> Handle(CreateDelive }; await dbContext.DeliveryChannelPolicies.AddAsync(newDeliveryChannelPolicy, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); - + try + { + await dbContext.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) when (ex.GetDatabaseError() is UniqueConstraintError) + { + return ModifyEntityResult.Failure( + $"A {request.DeliveryChannelPolicy.Channel}' policy called '{request.DeliveryChannelPolicy.Name}' already exists", + WriteResult.Conflict); + } + return ModifyEntityResult.Success(newDeliveryChannelPolicy, WriteResult.Created); } } diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesFromDeliveryChannel.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesForDeliveryChannel.cs similarity index 73% rename from src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesFromDeliveryChannel.cs rename to src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesForDeliveryChannel.cs index 32ed41897..4d20055ae 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesFromDeliveryChannel.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetPoliciesForDeliveryChannel.cs @@ -7,19 +7,19 @@ namespace API.Features.DeliveryChannels.Requests; -public class GetPoliciesFromDeliveryChannel: IRequest>> +public class GetPoliciesForDeliveryChannel: IRequest>> { public int CustomerId { get; } public string DeliveryChannelName { get; } - public GetPoliciesFromDeliveryChannel(int customerId, string deliveryChannelName) + public GetPoliciesForDeliveryChannel(int customerId, string deliveryChannelName) { CustomerId = customerId; DeliveryChannelName = deliveryChannelName; } } -public class GetDeliveryChannelPoliciesHandler : IRequestHandler>> +public class GetDeliveryChannelPoliciesHandler : IRequestHandler>> { private readonly DlcsContext dbContext; @@ -28,7 +28,7 @@ public GetDeliveryChannelPoliciesHandler(DlcsContext dbContext) this.dbContext = dbContext; } - public async Task>> Handle(GetPoliciesFromDeliveryChannel request, CancellationToken cancellationToken) + public async Task>> Handle(GetPoliciesForDeliveryChannel request, CancellationToken cancellationToken) { var deliveryChannelPolicies = await dbContext.DeliveryChannelPolicies .AsNoTracking() diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs index 4d27d7978..76ac60816 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/PatchDeliveryChannelPolicy.cs @@ -17,9 +17,9 @@ public class PatchDeliveryChannelPolicy : IRequest +/// Provides additional Database specific information about +/// a thrown by EF Core. +/// +/// The table involved, if any. +/// The constraint involved, if any. +/// The unwrapped database provider specific exception. +/// See https://haacked.com/archive/2022/12/12/specific-db-exception for more information +public record DbError(string? TableName, string? ConstraintName, Exception Exception); \ No newline at end of file diff --git a/src/protagonist/DLCS.Repository/Exceptions/DbUniqueConstraintError.cs b/src/protagonist/DLCS.Repository/Exceptions/DbUniqueConstraintError.cs new file mode 100644 index 000000000..d7a143877 --- /dev/null +++ b/src/protagonist/DLCS.Repository/Exceptions/DbUniqueConstraintError.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace DLCS.Repository.Exceptions; + +/// +/// Provides additional Postgres specific information about a +/// thrown by EF Core.This describes +/// the case where the exception is a unique constraint violation. +/// +/// The column names parsed from the constraint +/// name assuming the constraint follows the "IX_{Table}_{Column1}_..._{ColumnN}" naming convention. +/// The table involved, if any. +/// The constraint involved, if any. +/// The unwrapped database provider specific exception. +/// See https://haacked.com/archive/2022/12/12/specific-db-exception for more information. +public record UniqueConstraintError( + IReadOnlyList ColumnNames, + string? TableName, + string? ConstraintName, + Exception Exception) : DbError(TableName, ConstraintName, Exception) { + + /// + /// Creates a from a . + /// + /// The . + /// A with extra information about the unique constraint violation. + public static UniqueConstraintError FromPostgresException(PostgresException postgresException) + { + var constraintName = postgresException.ConstraintName; + var tableName = postgresException.TableName; + + var constrainPrefix = tableName != null ? $"IX_{tableName}_" : null; + + var columnNames = Array.Empty(); + + if (constrainPrefix != null + && constraintName != null + && constraintName.StartsWith(constrainPrefix, StringComparison.Ordinal)) + { + columnNames = constraintName[constrainPrefix.Length..].Split('_'); + } + + return new UniqueConstraintError(columnNames, tableName, constraintName, postgresException); + } +} \ No newline at end of file diff --git a/src/protagonist/DLCS.Repository/Exceptions/DbUpdateExceptionX.cs b/src/protagonist/DLCS.Repository/Exceptions/DbUpdateExceptionX.cs new file mode 100644 index 000000000..37cd97027 --- /dev/null +++ b/src/protagonist/DLCS.Repository/Exceptions/DbUpdateExceptionX.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace DLCS.Repository.Exceptions; + +/// +/// Extensions to used to retrieve more +/// database specific information about the thrown exception. +/// +/// See https://haacked.com/archive/2022/12/12/specific-db-exception for more information. +public static class DbUpdateExceptionX +{ + /// + /// Retrieves a with database specific error + /// information from the thrown by EF Core. + /// + /// The thrown. + /// A or derived class if the inner + /// exception matches one of the supported types. Otherwise returns null. + public static DbError? GetDatabaseError(this DbUpdateException exception) + { + if (exception.InnerException is PostgresException postgresException) + { + return postgresException.SqlState switch + { + PostgresErrorCodes.UniqueViolation => UniqueConstraintError + .FromPostgresException(postgresException), + //... Other error codes mapped to other error types. + _ => new DbError( + postgresException.TableName, + postgresException.ConstraintName, + postgresException) + }; + } + + return null; + } +} \ No newline at end of file From 301da79f3ab3694580e20dc09ade4d757d486fed Mon Sep 17 00:00:00 2001 From: griffri Date: Tue, 27 Feb 2024 15:36:17 +0000 Subject: [PATCH 35/35] Build delivery channel policy collections model in controller, change return type of GetDeliveryChannelPolicyCollections handler to Dictionary --- .../DeliveryChannelPoliciesController.cs | 15 ++++++- .../GetDeliveryChannelPolicyCollections.cs | 43 ++++++------------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs index bc40663c9..a5485d3f9 100644 --- a/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs +++ b/src/protagonist/API/Features/DeliveryChannels/DeliveryChannelPoliciesController.cs @@ -43,8 +43,19 @@ public class DeliveryChannelPoliciesController : HydraController { var request = new GetDeliveryChannelPolicyCollections(customerId, Request.GetDisplayUrl(Request.Path), Request.GetJsonLdId()); var result = await Mediator.Send(request, cancellationToken); - - return Ok(result); + var policyCollections = new HydraCollection>() + { + WithContext = true, + Members = result.Select(c => + new HydraNestedCollection(request.BaseUrl, c.Key) + { + Title = c.Value, + }).ToArray(), + TotalItems = result.Count, + Id = request.JsonLdId, + }; + + return Ok(policyCollections); } /// diff --git a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs index 772f595bc..04067a265 100644 --- a/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs +++ b/src/protagonist/API/Features/DeliveryChannels/Requests/GetDeliveryChannelPolicyCollections.cs @@ -1,12 +1,11 @@ -using DLCS.Model.Assets; -using DLCS.Model.Policies; +using System.Collections.Generic; +using DLCS.Model.Assets; using DLCS.Repository; -using Hydra.Collections; using MediatR; namespace API.Features.DeliveryChannels.Requests; -public class GetDeliveryChannelPolicyCollections: IRequest>> +public class GetDeliveryChannelPolicyCollections: IRequest> { public int CustomerId { get; } public string BaseUrl { get; } @@ -20,7 +19,7 @@ public GetDeliveryChannelPolicyCollections(int customerId, string baseUrl, strin } } -public class GetDeliveryChannelPolicyCollectionsHandler : IRequestHandler>> +public class GetDeliveryChannelPolicyCollectionsHandler : IRequestHandler> { private readonly DlcsContext dbContext; @@ -29,34 +28,16 @@ public GetDeliveryChannelPolicyCollectionsHandler(DlcsContext dbContext) this.dbContext = dbContext; } - public async Task>> Handle(GetDeliveryChannelPolicyCollections request, CancellationToken cancellationToken) + public async Task> Handle(GetDeliveryChannelPolicyCollections request, CancellationToken cancellationToken) { - var policyCollections = new HydraNestedCollection[] + var policyCollections = new Dictionary() { - new(request.BaseUrl, AssetDeliveryChannels.Image) - { - Title = "Policies for IIIF Image service delivery", - }, - new(request.BaseUrl, AssetDeliveryChannels.Thumbnails) - { - Title = "Policies for thumbnails as IIIF Image Services", - }, - new(request.BaseUrl, AssetDeliveryChannels.Timebased) - { - Title = "Policies for Audio and Video delivery", - }, - new(request.BaseUrl, AssetDeliveryChannels.File) - { - Title = "Policies for File delivery", - } - }; - - return new HydraCollection>() - { - WithContext = true, - Members = policyCollections, - TotalItems = policyCollections.Length, - Id = request.JsonLdId, + {AssetDeliveryChannels.Image, "Policies for IIIF Image service delivery"}, + {AssetDeliveryChannels.Thumbnails, "Policies for thumbnails as IIIF Image Services"}, + {AssetDeliveryChannels.Timebased, "Policies for Audio and Video delivery"}, + {AssetDeliveryChannels.File, "Policies for File delivery"} }; + + return policyCollections; } } \ No newline at end of file