Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ability to create a customer with attached delivery channels #727

Merged
merged 13 commits into from
Feb 14, 2024
Merged
75 changes: 68 additions & 7 deletions src/protagonist/API.Tests/Integration/CustomerTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand All @@ -6,6 +7,7 @@
using API.Client;
using API.Tests.Integration.Infrastructure;
using DLCS.HydraModel;
using DLCS.Model.Policies;
using DLCS.Repository;
using Hydra.Collections;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -64,17 +66,14 @@ public async Task GetOtherCustomer_Returns_NotFound()
[Fact]
public async Task Create_Customer_Test()
{
// arrange
// Need to create an entity counter global for customers
var expectedNewCustomerId = 1;
var expectedNewCustomerId = 2;

var customerCounter = await dbContext.EntityCounters.SingleOrDefaultAsync(ec
=> ec.Customer == 0 && ec.Scope == "0" && ec.Type == "customer");
customerCounter.Should().BeNull();
// this is true atm but Seed data might change this.
// The counter should be created on first use, see below
customerCounter.Should().NotBeNull();

const string newCustomerJson = @"{
const string newCustomerJson = @"{
""@type"": ""Customer"",
""name"": ""my-new-customer"",
""displayName"": ""My New Customer""
Expand Down Expand Up @@ -146,6 +145,9 @@ public async Task Create_Customer_Test()
var priorityQueue =
await dbContext.Queues.SingleAsync(q => q.Customer == expectedNewCustomerId && q.Name == "priority");
priorityQueue.Size.Should().Be(0);

dbContext.DeliveryChannelPolicies.Count(d => d.Customer == newDbCustomer.Id).Should().Be(3);
dbContext.DefaultDeliveryChannels.Count(d => d.Customer == newDbCustomer.Id).Should().Be(5);
}

[Fact]
Expand All @@ -166,7 +168,66 @@ public async Task CreateNewCustomer_Throws_IfNameConflicts()
// assert
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
}

[Fact]
public async Task NewlyCreatedCustomer_RollsBackSuccessfully_WhenDeliveryChannelsNotCreatedSuccessfully()
{
const int expectedCustomerId = 2;

var url = $"/customers";
const string customerJson = @"{
""name"": ""apiTest2"",
""displayName"": ""testing api customer 2""
}";
var content = new StringContent(customerJson, Encoding.UTF8, "application/json");

dbContext.DeliveryChannelPolicies.Add(new DeliveryChannelPolicy()
{
Id = 250,
DisplayName = "A default audio policy",
Name = "default-audio",
Customer = expectedCustomerId,
Channel = "iiif-av",
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
PolicyData = null,
System = false
}); // creates a duplicate policy, causing an error

await dbContext.SaveChangesAsync();


// Act
var response = await httpClient.AsAdmin(1).PostAsync(url, content);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
dbContext.DeliveryChannelPolicies.Count(d => d.Customer == expectedCustomerId).Should().Be(1); //difference of 1 due to delivery channel added above
dbContext.DefaultDeliveryChannels.Count(d => d.Customer == expectedCustomerId).Should().Be(0);
dbContext.Customers.FirstOrDefault(c => c.Id == expectedCustomerId).Should().BeNull();
dbContext.EntityCounters.Count(e => e.Customer == expectedCustomerId).Should().Be(0);
dbContext.Roles.Count(r => r.Customer == expectedCustomerId).Should().Be(0);
}

private async Task EnsureAdminCustomerCreated()
{
var adminCustomer = await dbContext.Customers.SingleOrDefaultAsync(c => c.Id == 1);
if (adminCustomer == null)
{
// Setup a customer 1, which is required for the customer in delivery channels
dbContext.Customers.Add(new DLCS.Model.Customers.Customer()
{
Id = 1,
Name = "admin",
DisplayName = "admin customer",
Created = DateTime.UtcNow,
Keys = new[] { "some", "keys" },
Administrator = true,
AcceptedAgreement = true
});
dbContext.SaveChanges();
}
}

[Fact]
public async Task CreateNewCustomer_Returns400_IfNameStartsWithVersion()
{
Expand Down
2 changes: 1 addition & 1 deletion src/protagonist/API.Tests/Integration/SpaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ public async Task Put_Space_Leaves_Omitted_Fields_Intact()
{
return spaceTestCustomer.Id;
}

string spaceTestCustomerJson = $@"{{
""@type"": ""Customer"",
""name"": ""{customerName}"",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Data;
using DLCS.Model;
using DLCS.Model.Auth;
using DLCS.Model.DeliveryChannels;
using DLCS.Model.Processing;
using DLCS.Repository;
using DLCS.Repository.Entities;
Expand Down Expand Up @@ -44,15 +46,21 @@ public class CreateCustomerHandler : IRequestHandler<CreateCustomer, CreateCusto
private readonly DlcsContext dbContext;
private readonly IEntityCounterRepository entityCounterRepository;
private readonly IAuthServicesRepository authServicesRepository;
private readonly IDeliveryChannelPolicyRepository deliveryChannelPolicyRepository;
private readonly IDefaultDeliveryChannelRepository defaultDeliveryChannelRepository;

public CreateCustomerHandler(
DlcsContext dbContext,
IEntityCounterRepository entityCounterRepository,
IAuthServicesRepository authServicesRepository)
IAuthServicesRepository authServicesRepository,
IDeliveryChannelPolicyRepository deliveryChannelPolicyRepository,
IDefaultDeliveryChannelRepository defaultDeliveryChannelRepository)
{
this.dbContext = dbContext;
this.entityCounterRepository = entityCounterRepository;
this.authServicesRepository = authServicesRepository;
this.deliveryChannelPolicyRepository = deliveryChannelPolicyRepository;
this.defaultDeliveryChannelRepository = defaultDeliveryChannelRepository;
}

public async Task<CreateCustomerResult> Handle(CreateCustomer request, CancellationToken cancellationToken)
Expand All @@ -64,6 +72,9 @@ public async Task<CreateCustomerResult> Handle(CreateCustomer request, Cancellat
await EnsureCustomerNamesNotTaken(request, result, cancellationToken);
if (result.ErrorMessages.Any()) return result;

await using var transaction =
await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

var newModelId = await GetIdForNewCustomer();
result.Customer = await CreateCustomer(request, cancellationToken, newModelId);

Expand Down Expand Up @@ -96,8 +107,29 @@ await dbContext.Queues.AddRangeAsync(
new Queue { Customer = result.Customer.Id, Name = QueueNames.Default, Size = 0 },
new Queue { Customer = result.Customer.Id, Name = QueueNames.Priority, Size = 0 }
);
await dbContext.SaveChangesAsync(cancellationToken);

var deliveryChannelPoliciesCreated = await deliveryChannelPolicyRepository.AddDeliveryChannelCustomerPolicies(result.Customer.Id,
cancellationToken);
var defaultDeliveryChannelsCreated = await defaultDeliveryChannelRepository.AddCustomerDefaultDeliveryChannels(result.Customer.Id,
cancellationToken);

if (deliveryChannelPoliciesCreated && defaultDeliveryChannelsCreated)
{
await transaction.CommitAsync(cancellationToken);
return result;
}

result = new CreateCustomerResult()
{
ErrorMessages = new List<string>()
{
"Failed to create customer"
}
};

await transaction.RollbackAsync(cancellationToken);


// [UpdateCustomerBehaviour] - customer has already been saved.
// The problem here is that we have had:
// - some direct use of dbContext
Expand Down
4 changes: 4 additions & 0 deletions src/protagonist/API/Infrastructure/ServiceCollectionX.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using DLCS.Model.Assets;
using DLCS.Model.Auth;
using DLCS.Model.Customers;
using DLCS.Model.DeliveryChannels;
using DLCS.Model.PathElements;
using DLCS.Model.Policies;
using DLCS.Model.Processing;
Expand All @@ -21,6 +22,7 @@
using DLCS.Repository.Assets;
using DLCS.Repository.Auth;
using DLCS.Repository.Customers;
using DLCS.Repository.DeliveryChannels;
using DLCS.Repository.Entities;
using DLCS.Repository.Policies;
using DLCS.Repository.Processing;
Expand Down Expand Up @@ -99,6 +101,8 @@ public static IServiceCollection AddDataAccess(this IServiceCollection services,
.AddSingleton<ICustomerRepository, DapperCustomerRepository>()
.AddSingleton<IAuthServicesRepository, DapperAuthServicesRepository>()
.AddScoped<IPolicyRepository, PolicyRepository>()
.AddScoped<IDeliveryChannelPolicyRepository, DeliveryChannelPolicyRepository>()
.AddScoped<IDefaultDeliveryChannelRepository, DefaultDeliveryChannelRepository>()
.AddDlcsContext(configuration);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;

namespace DLCS.Model.DeliveryChannels;

public interface IDefaultDeliveryChannelRepository
{
public Task<bool> AddCustomerDefaultDeliveryChannels(int customerId, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using DLCS.Model.Policies;

namespace DLCS.Model.DeliveryChannels;

public interface IDeliveryChannelPolicyRepository
{
public Task<DeliveryChannelPolicy?> GetDeliveryChannelPolicy(int customerId, string policyName, string channel,
CancellationToken cancellationToken);

public Task<bool> AddDeliveryChannelCustomerPolicies(int customerId,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DLCS.Core.Caching;
using DLCS.Model.DeliveryChannels;
using LazyCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace DLCS.Repository.DeliveryChannels;

public class DefaultDeliveryChannelRepository : IDefaultDeliveryChannelRepository
{
private readonly IAppCache appCache;
private readonly CacheSettings cacheSettings;
private readonly ILogger<DefaultDeliveryChannelRepository> logger;
private readonly IDeliveryChannelPolicyRepository deliveryChannelPolicyRepository;
private readonly DlcsContext dlcsContext;
private const int SystemCustomerId = 1;
private const int SystemSpaceId = 0;
const string AppCacheKey = "DefaultDeliveryChannels";

public DefaultDeliveryChannelRepository(
IAppCache appCache,
ILogger<DefaultDeliveryChannelRepository> logger,
IOptions<CacheSettings> cacheOptions,
IDeliveryChannelPolicyRepository deliveryChannelPolicyRepository,
DlcsContext dlcsContext)
{
this.appCache = appCache;
this.logger = logger;
cacheSettings = cacheOptions.Value;
this.deliveryChannelPolicyRepository = deliveryChannelPolicyRepository;
this.dlcsContext = dlcsContext;
}

public async Task<bool> AddCustomerDefaultDeliveryChannels(int customerId, CancellationToken cancellationToken = default)
{
try
{
var defaultDeliveryChannelsToCopy = await GetDefaultDeliveryChannelsForSystemCustomer(cancellationToken);

var updatedPolicies = defaultDeliveryChannelsToCopy.Select(async defaultDeliveryChannel => new DefaultDeliveryChannel()
{
Id = Guid.NewGuid(),
DeliveryChannelPolicyId = await GetCorrectDeliveryChannelId(customerId, defaultDeliveryChannel, cancellationToken),
MediaType = defaultDeliveryChannel.MediaType,
Customer = customerId,
Space = defaultDeliveryChannel.Space

}).Select(t => t.Result).ToList();

await dlcsContext.DefaultDeliveryChannels.AddRangeAsync(updatedPolicies, cancellationToken);

var updated = await dlcsContext.SaveChangesAsync(cancellationToken);

if (updated > 0)
{
appCache.Remove(AppCacheKey);
}
}
catch (Exception e)
{
dlcsContext.ChangeTracker.Clear();
logger.LogError(e, "Error adding delivery channel policies to customer {Customer}", customerId);
return false;
}

return true;
}

private async Task<List<DefaultDeliveryChannel>> GetDefaultDeliveryChannelsForSystemCustomer(CancellationToken cancellationToken)
{
return await appCache.GetOrAddAsync(AppCacheKey, async () =>
{
logger.LogDebug("Refreshing DefaultDeliveryChannels from database");
var defaultDeliveryChannels =
await dlcsContext.DefaultDeliveryChannels.AsNoTracking().Where(p => p.Customer == SystemCustomerId && p.Space == SystemSpaceId).ToListAsync(cancellationToken: cancellationToken);
return defaultDeliveryChannels;
}, cacheSettings.GetMemoryCacheOptions());
}

private async Task<int> GetCorrectDeliveryChannelId(int customerId, DefaultDeliveryChannel defaultDeliveryChannel, CancellationToken cancellationToken)
{
int deliveryChannelPolicyId;

switch (defaultDeliveryChannel.MediaType)
{
case "audio/*":
var audioPolicy = await deliveryChannelPolicyRepository.GetDeliveryChannelPolicy(customerId,
"default-audio", "iiif-av", cancellationToken);
deliveryChannelPolicyId = audioPolicy!.Id;
break;
case "video/*":
var videoPolicy = await deliveryChannelPolicyRepository.GetDeliveryChannelPolicy(customerId,
"default-video", "iiif-av", cancellationToken);
deliveryChannelPolicyId = videoPolicy!.Id;
break;
case "image/*":
deliveryChannelPolicyId = await GetPolicyForImageMediaType(customerId, defaultDeliveryChannel, cancellationToken);
break;
default:
deliveryChannelPolicyId = defaultDeliveryChannel.DeliveryChannelPolicyId;
break;
}

return deliveryChannelPolicyId;
}

private async Task<int> GetPolicyForImageMediaType(int customerId, DefaultDeliveryChannel defaultDeliveryChannel,
CancellationToken cancellationToken)
{
int deliveryChannelPolicyId;
if (defaultDeliveryChannel.DeliveryChannelPolicyId != 1) // 1 has to be a iiif-img policy for a customer
{
var thumbsPolicy = await deliveryChannelPolicyRepository.GetDeliveryChannelPolicy(customerId,
"default", "thumbs", cancellationToken);
deliveryChannelPolicyId = thumbsPolicy!.Id;
}
else
{
deliveryChannelPolicyId = defaultDeliveryChannel.DeliveryChannelPolicyId;
}

return deliveryChannelPolicyId;
}
}
Loading