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
2 changes: 1 addition & 1 deletion src/protagonist/API/API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.22" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System.Collections.Generic;
using System.Data;
using DLCS.Model;
using DLCS.Model.Auth;
using DLCS.Model.DeliveryChannels;
using DLCS.Model.Policies;
using DLCS.Model.Processing;
using DLCS.Repository;
using DLCS.Repository.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace API.Features.Customer.Requests;

Expand Down Expand Up @@ -44,15 +48,23 @@ public class CreateCustomerHandler : IRequestHandler<CreateCustomer, CreateCusto
private readonly DlcsContext dbContext;
private readonly IEntityCounterRepository entityCounterRepository;
private readonly IAuthServicesRepository authServicesRepository;
private readonly IDeliveryChannelPolicyRepository deliveryChannelPolicyRepository;
private readonly ILogger<CreateCustomerHandler> logger;
private const int SystemCustomerId = 1;
private const int SystemSpaceId = 0;

public CreateCustomerHandler(
DlcsContext dbContext,
IEntityCounterRepository entityCounterRepository,
IAuthServicesRepository authServicesRepository)
IAuthServicesRepository authServicesRepository,
IDeliveryChannelPolicyRepository deliveryChannelPolicyRepository,
ILogger<CreateCustomerHandler> logger)
{
this.dbContext = dbContext;
this.entityCounterRepository = entityCounterRepository;
this.authServicesRepository = authServicesRepository;
this.deliveryChannelPolicyRepository = deliveryChannelPolicyRepository;
this.logger = logger;
}

public async Task<CreateCustomerResult> Handle(CreateCustomer request, CancellationToken cancellationToken)
Expand All @@ -64,6 +76,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 +111,31 @@ 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);

var defaultDeliveryChannelsCreated = await 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 Expand Up @@ -165,4 +203,53 @@ private async Task<int> GetIdForNewCustomer()

return newModelId;
}

private async Task<bool> AddCustomerDefaultDeliveryChannels(int customerId, CancellationToken cancellationToken = default)
{
try
{
await dbContext.DefaultDeliveryChannels.Where(
p => p.Customer == SystemCustomerId &&
p.Space == SystemSpaceId).InsertFromQueryAsync("\"DefaultDeliveryChannels\"", defaultDeliveryChannel => new DefaultDeliveryChannel
{
Id = Guid.NewGuid(),
DeliveryChannelPolicyId = defaultDeliveryChannel.DeliveryChannelPolicyId,
MediaType = defaultDeliveryChannel.MediaType,
Customer = customerId,
Space = defaultDeliveryChannel.Space
}, cancellationToken);


var customerSpecificPolicies = new Dictionary<string, (DeliveryChannelPolicy deliveryChannelPolicy, int initialPolicyNumber)>
{
{
"audio", (deliveryChannelPolicyRepository.GetDeliveryChannelPolicy(customerId,
"default-audio", "iiif-av", cancellationToken).Result!, 5) // 5 = customer 1 iiif-av audio policy
},
{
"video", (deliveryChannelPolicyRepository.GetDeliveryChannelPolicy(customerId,
"default-video", "iiif-av", cancellationToken).Result!, 6) // 6 = customer 1 iiif-av video policy
},
{ "thumbs", (deliveryChannelPolicyRepository.GetDeliveryChannelPolicy(customerId,
"default", "thumbs", cancellationToken).Result!, 3 )} // 3 = customer 1 default thumbs policy
};

foreach (var customerPolicy in customerSpecificPolicies)
{
await dbContext.DefaultDeliveryChannels.AsNoTracking().Where(d => d.Customer == customerId &&
d.Space == SystemSpaceId &&
d.DeliveryChannelPolicyId == customerPolicy.Value.initialPolicyNumber)
.UpdateFromQueryAsync(d => new DefaultDeliveryChannel()
{ DeliveryChannelPolicyId = customerPolicy.Value.deliveryChannelPolicy.Id }, cancellationToken);
}
}
catch (Exception e)
{
dbContext.ChangeTracker.Clear();
logger.LogError(e, "Error adding delivery channel policies to customer {Customer}", customerId);
return false;
}

return true;
}
}
3 changes: 3 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,7 @@ public static IServiceCollection AddDataAccess(this IServiceCollection services,
.AddSingleton<ICustomerRepository, DapperCustomerRepository>()
.AddSingleton<IAuthServicesRepository, DapperAuthServicesRepository>()
.AddScoped<IPolicyRepository, PolicyRepository>()
.AddScoped<IDeliveryChannelPolicyRepository, DeliveryChannelPolicyRepository>()
.AddDlcsContext(configuration);

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

namespace DLCS.Model.DeliveryChannels;

public interface IDeliveryChannelPolicyRepository
{
/// <summary>
/// Retrieves a specific delivery channel policy
/// </summary>
/// <param name="customerId">The id of the customer used to retrieve a policy</param>
/// <param name="policyName">The name of the policy to retrieve</param>
/// <param name="channel">The channel of the policy to retrieve</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A delivery channel policy</returns>
public Task<DeliveryChannelPolicy?> GetDeliveryChannelPolicy(int customerId, string policyName, string channel,
CancellationToken cancellationToken);

/// <summary>
/// Adds delivery channel policies to the table for a customer
/// </summary>
/// <param name="customerId">The customer id to create the policies for</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>Whether creating the new policies was successful or not</returns>
public Task<bool> AddDeliveryChannelCustomerPolicies(int customerId,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Linq;
using DLCS.Core.Caching;
using DLCS.Repository.DeliveryChannels;
using LazyCache.Mocks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Test.Helpers.Integration;

namespace DLCS.Repository.Tests;

[Trait("Category", "Database")]
[Collection(DatabaseCollection.CollectionName)]
public class DeliveryChannelPolicyRepositoryTests
{
private readonly DlcsContext dbContext;
private readonly DeliveryChannelPolicyRepository sut;

public DeliveryChannelPolicyRepositoryTests(DlcsDatabaseFixture dbFixture)
{
dbContext = dbFixture.DbContext;
dbFixture.CleanUp();

var cacheSettings = Options.Create(new CacheSettings());
sut = new DeliveryChannelPolicyRepository(new MockCachingService(), new NullLogger<DeliveryChannelPolicyRepository>(), cacheSettings,
dbContext);
}

[Fact]
public async Task GetDeliveryChannelPolicy_ReturnsAPolicy()
{
// Arrange and Act
var policy = await sut.GetDeliveryChannelPolicy(1, "default", "iiif-img");

// Assert
policy.Should().NotBeNull();
policy.Channel.Should().Be("iiif-img");
policy.Id.Should().Be(1);
}

[Fact]
public async Task GetDeliveryChannelPolicy_ReturnsNull_WhenNoPolicyFound()
{
// Arrange and Act
var policy = await sut.GetDeliveryChannelPolicy(1, "no-policy", "iiif-img");

// Assert
policy.Should().BeNull();
}

[Fact]
public async Task AddDeliveryChannelPolicies_CreatesCorrectPolicies()
{
// Arrange and Act
var policiesCreated = await sut.AddDeliveryChannelCustomerPolicies(100);

var policies = dbContext.DeliveryChannelPolicies.Where(d => d.Customer == 100);

// Assert
policiesCreated.Should().BeTrue();
policies.Count().Should().Be(3);
policies.Should().ContainSingle(p => p.Channel == "thumbs");
policies.Should().ContainSingle(p => p.Name == "default-audio");
policies.Should().ContainSingle(p => p.Name == "default-video");
policies.Should().NotContain(p => p.Channel == "iiif-img");
policies.Should().NotContain(p => p.Channel == "file");
}
}
Loading
Loading