From bddb2e6b3a748cbbc3de41504b2636193007e2f0 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Tue, 25 Jul 2023 09:24:00 +0100 Subject: [PATCH] changing line endings --- .../API.Tests/Integration/SpaceTests.cs | 880 +++++++++--------- .../API/Infrastructure/HydraController.cs | 686 +++++++------- .../DLCS.Model/Spaces/ISpaceRepository.cs | 52 +- 3 files changed, 809 insertions(+), 809 deletions(-) diff --git a/src/protagonist/API.Tests/Integration/SpaceTests.cs b/src/protagonist/API.Tests/Integration/SpaceTests.cs index e31698550..a459bab38 100644 --- a/src/protagonist/API.Tests/Integration/SpaceTests.cs +++ b/src/protagonist/API.Tests/Integration/SpaceTests.cs @@ -1,441 +1,441 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using API.Client; -using API.Tests.Integration.Infrastructure; -using DLCS.Core.Types; -using DLCS.HydraModel; -using DLCS.Repository; -using DLCS.Repository.Entities; -using Hydra; -using Hydra.Collections; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Linq; -using Test.Helpers.Integration; -using Test.Helpers.Integration.Infrastructure; - -namespace API.Tests.Integration; - -[Trait("Category", "Integration")] -[Collection(CollectionDefinitions.DatabaseCollection.CollectionName)] -public class SpaceTests : IClassFixture> -{ - private readonly DlcsContext dbContext; - private readonly HttpClient httpClient; - - public SpaceTests(DlcsDatabaseFixture dbFixture, ProtagonistAppFactory factory) - { - dbContext = dbFixture.DbContext; - httpClient = factory.ConfigureBasicAuthedIntegrationTestHttpClient(dbFixture, "API-Test"); - dbFixture.CleanUp(); - } - - [Fact] - public async Task Post_SimpleSpace_Creates_Space() - { - // arrange - int? customerId = 99; - var counter = await dbContext.EntityCounters.SingleAsync( - ec => ec.Customer == 99 && ec.Scope == "99" && ec.Type == "space"); - int expectedSpace = (int) counter.Next; - - const string newSpaceJson = @"{ - ""@type"": ""Space"", - ""name"": ""Test Space"" -}"; - // act - var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); - var postUrl = $"/customers/{customerId}/spaces"; - var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); - var apiSpace = await response.ReadAsHydraResponseAsync(); - - // assert - response.StatusCode.Should().Be(HttpStatusCode.Created); - response.Headers.Location.PathAndQuery.Should().Be($"{postUrl}/{expectedSpace}"); - apiSpace.Should().NotBeNull(); - apiSpace.Name.Should().Be("Test Space"); - apiSpace.MaxUnauthorised.Should().Be(-1); - } - - [Fact] - public async Task Post_ComplexSpace_Creates_Space() - { - // arrange - int? customerId = 99; // await EnsureCustomerForSpaceTests("Post_ComplexSpace_Creates_Space"); - - const string newSpaceJson = @"{ - ""@type"": ""Space"", - ""name"": ""Test Complex Space"", - ""defaultRoles"": [""role1"", ""role2""], - ""defaultTags"": [""tag1"", ""tag2""], - ""maxUnauthorised"": 400 -}"; - // act - var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); - var postUrl = $"/customers/{customerId}/spaces"; - var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); - var apiSpace = await response.ReadAsHydraResponseAsync(); - - // assert - apiSpace.Should().NotBeNull(); - AssertSpace(apiSpace); - - // verify that we can re-obtain the space with GET - var newResponse = await httpClient.AsCustomer(customerId.Value).GetAsync(apiSpace.Id); - var reObtainedSpace = await newResponse.ReadAsHydraResponseAsync(); - AssertSpace(reObtainedSpace); - - void AssertSpace(Space space) - { - space.Name.Should().Be("Test Complex Space"); - space.DefaultRoles.Should().BeEquivalentTo("role1", "role2"); - space.DefaultTags.Should().BeEquivalentTo("tag1", "tag2"); - space.MaxUnauthorised.Should().Be(400); - } - } - - [Fact] - public async Task Create_Space_Updates_EntityCounters() - { - // After creating a space in Deliverator: - - // EXISTING COUNTER: was: - // {'Type': 'space', 'Scope': '2', 'Next': 35, 'Customer': 2} - // now: - // {'Type': 'space', 'Scope': '2', 'Next': 36, 'Customer': 2} - - // NEW COUNTER - // {'Type': 'space-images', 'Scope': '35', 'Next': 0, 'Customer': 2} - - // That last one seems wrong, should be Next = 1, not 0 - - int? customerId = await EnsureCustomerForSpaceTests(); - - var currentCounter = await dbContext.EntityCounters.SingleAsync( - ec => ec.Type == "space" && ec.Scope == customerId.ToString() && ec.Customer == customerId); - - var next = (int)currentCounter.Next; - const string newSpaceJson = @"{ - ""@type"": ""Space"", - ""name"": ""Entity Counter Test Space"" -}"; - - var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); - var postUrl = $"/customers/{customerId}/spaces"; - var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); - var apiSpace = await response.ReadAsHydraResponseAsync(); - - apiSpace.Id.Should().EndWith($"{postUrl}/{next}"); - currentCounter = await dbContext.EntityCounters.SingleAsync( - ec => ec.Type == "space" && ec.Scope == customerId.ToString() && ec.Customer == customerId); - currentCounter.Next.Should().Be(next + 1); - var spaceImageCounter = await dbContext.EntityCounters.SingleOrDefaultAsync( - ec => - ec.Type == KnownEntityCounters.SpaceImages && - ec.Customer == customerId.Value && - ec.Scope == next.ToString()); - spaceImageCounter.Should().NotBeNull(); - spaceImageCounter.Next.Should().Be(1); // Deliverator makes 0 here. But that doesn't feel right! - } - - [Fact] - public async Task GetSpaces_Returns_HydraCollection() - { - // arrange - int? customerId = await EnsureCustomerForSpaceTests("hydracollection_space"); - await EnsureSpaces(customerId.Value, 10); - var spacesUrl = $"/customers/{customerId.Value}/spaces"; - - // act - var response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - - // assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - var coll = await response.ReadAsHydraResponseAsync>(); - coll.Should().NotBeNull(); - coll.Type.Should().Be("Collection"); - coll.Members.Should().HaveCount(10); - coll.Members.Should().Contain(jo => jo["@id"].Value().EndsWith($"{spacesUrl}/1")); - coll.Members.Should().Contain(jo => jo["@id"].Value().EndsWith($"{spacesUrl}/10")); - } - - [Fact] - public async Task Paged_Requests_Return_Correct_Views() - { - // arrange - int? customerId = await EnsureCustomerForSpaceTests("hydracollection_space"); - await EnsureSpaces(customerId.Value, 55); - // set a pageSize of 10 - var spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10"; - - // act - var response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - - // assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - var coll = await response.ReadAsHydraResponseAsync>(); - coll.Should().NotBeNull(); - coll.Type.Should().Be("Collection"); - coll.Members.Should().HaveCount(10); - coll.PageSize.Should().Be(10); - coll.View.Should().NotBeNull(); - coll.View.Page.Should().Be(1); - coll.View.Previous.Should().BeNull(); - coll.View.Next.Should().Contain("page=2"); - coll.View.TotalPages.Should().Be(6); - int pageCounter = 1; - var view = coll.View; - while (view.Next != null) - { - var nextResp = await httpClient.AsCustomer(customerId.Value).GetAsync(view.Next); - var nextColl = await nextResp.ReadAsHydraResponseAsync>(); - view = nextColl.View; - view.Previous.Should().Contain("page=" + pageCounter); - pageCounter++; - if (pageCounter < 6) - { - nextColl.Members.Should().HaveCount(10); - view.Next.Should().Contain("page=" + (pageCounter + 1)); - } - else - { - nextColl.Members.Should().HaveCount(5); - view.Next.Should().BeNull(); - } - } - } - - [Fact] - public async Task Paged_Requests_Support_Ordering() - { - // arrange - int? customerId = await EnsureCustomerForSpaceTests("hydracollection_space"); - await EnsureSpaces(customerId.Value, 25); - - var spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderBy=name"; - - // act - var response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - - // assert - var coll = await response.ReadAsHydraResponseAsync>(); - coll.Members[0]["name"].ToString().Should().Be("Space 0001"); - coll.Members[1]["name"].ToString().Should().Be("Space 0002"); - - spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderByDescending=name"; - response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - coll = await response.ReadAsHydraResponseAsync>(); - coll.Members[0]["name"].ToString().Should().Be("Space 0025"); - coll.Members[1]["name"].ToString().Should().Be("Space 0024"); - - var nextPage = await httpClient.AsCustomer(customerId.Value).GetAsync(coll.View.Next); - coll = await nextPage.ReadAsHydraResponseAsync>(); - coll.Members[0]["name"].ToString().Should().Be("Space 0015"); - coll.Members[1]["name"].ToString().Should().Be("Space 0014"); - - // Add another space just to be sure we are testing created properly - const string newSpaceJson = @"{ - ""@type"": ""Space"", - ""name"": ""Aardvark space"" -}"; - var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); - var postUrl = $"/customers/{customerId}/spaces"; - await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); - - spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderBy=name"; - response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - coll = await response.ReadAsHydraResponseAsync>(); - coll.Members[0]["name"].ToString().Should().Be("Aardvark space"); - - spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderBy=created"; - response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - coll = await response.ReadAsHydraResponseAsync>(); - coll.Members[0]["name"].ToString().Should().Be("Space 0001"); - - spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderByDescending=created"; - response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); - coll = await response.ReadAsHydraResponseAsync>(); - coll.Members[0]["name"].ToString().Should().Be("Aardvark space"); - - } - - [Fact] - public async Task Patch_Space_Updates_Name() - { - int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); - await dbContext.Spaces.AddTestSpace(customerId.Value, 1, "Patch Space Before"); - await dbContext.SaveChangesAsync(); - - const string patchJson = @"{ -""@type"": ""Space"", -""name"": ""Patch Space After"" -}"; - var patchContent = new StringContent(patchJson, Encoding.UTF8, "application/json"); - var patchUrl = $"/customers/{customerId}/spaces/1"; - var patchResponse = await httpClient.AsCustomer(customerId.Value).PatchAsync(patchUrl, patchContent); - var patchedSpace = await patchResponse.ReadAsHydraResponseAsync(); - - patchResponse.StatusCode.Should().Be(HttpStatusCode.OK); - patchedSpace.Name.Should().Be("Patch Space After"); - } - - [Fact] - public async Task Patch_Space_Prevents_Name_Conflict() - { - int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Prevents_Name_Conflict"); - await dbContext.Spaces.AddTestSpace(customerId.Value, 1, "Patch Space Name 1"); - await dbContext.Spaces.AddTestSpace(customerId.Value, 2, "Patch Space Name 2"); - await dbContext.SaveChangesAsync(); - - const string patchJson = @"{ -""@type"": ""Space"", -""name"": ""Patch Space Name 2"" -}"; - var patchContent = new StringContent(patchJson, Encoding.UTF8, "application/json"); - var patchUrl = $"/customers/{customerId}/spaces/1"; - var patchResponse = await httpClient.AsCustomer(customerId.Value).PatchAsync(patchUrl, patchContent); - patchResponse.StatusCode.Should().Be(HttpStatusCode.Conflict); - } - - [Fact] - public async Task Patch_Space_Leaves_Omitted_Fields_Intact() - { - // arrange - int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Leaves_Omitted_Fields_Intact"); - - const string newSpaceJson = @"{ - ""@type"": ""Space"", - ""name"": ""Patch Complex Space"", - ""defaultRoles"": [""role1"", ""role2""], - ""defaultTags"": [""tag1"", ""tag2""], - ""maxUnauthorised"": 400 - }"; - const string patchJson = @"{ - ""@type"": ""Space"", - ""name"": ""Patch Complex Space After"" - }"; - - // act - var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); - var postUrl = $"/customers/{customerId}/spaces"; - var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); - var apiSpace = await response.ReadAsHydraResponseAsync(); - - // assert - var patchContent = new StringContent(patchJson, Encoding.UTF8, "application/json"); - var patchResponse = await httpClient.AsCustomer(customerId.Value).PatchAsync(apiSpace.Id, patchContent); - var patchedSpace = await patchResponse.ReadAsHydraResponseAsync(); - - patchedSpace.Name.Should().Be("Patch Complex Space After"); - patchedSpace.DefaultRoles.Should().BeEquivalentTo("role1", "role2"); - patchedSpace.DefaultTags.Should().BeEquivalentTo("tag1", "tag2"); - patchedSpace.MaxUnauthorised.Should().Be(400); - } - - private async Task EnsureCustomerForSpaceTests(string customerName = "space-test-customer") - { - var spaceTestCustomer = await dbContext.Customers.SingleOrDefaultAsync(c => c.Name == customerName); - - if (spaceTestCustomer != null) - { - return spaceTestCustomer.Id; - } - - string spaceTestCustomerJson = $@"{{ - ""@type"": ""Customer"", - ""name"": ""{customerName}"", - ""displayName"": ""Display - {customerName}"" -}}"; - - var content = new StringContent(spaceTestCustomerJson, Encoding.UTF8, "application/json"); - var response = await httpClient.AsAdmin().PostAsync("/customers", content); - var apiCustomer = await response.ReadAsHydraResponseAsync(); - return apiCustomer?.Id.GetLastPathElementAsInt(); - } - - /// - /// Create lots of spaces to test paging - /// - /// - private async Task EnsureSpaces(int customerId, int numberOfSpaces) - { - var seed = DateTime.Now.Ticks.ToString(); - const string newSpaceJsonTemplate = @"{ - ""@type"": ""Space"", - ""name"": ""{space-name}"" -}"; - var postUrl = $"/customers/{customerId}/spaces"; - - for (int i = 1; i <= numberOfSpaces; i++) - { - var spaceName = $"Space {i.ToString().PadLeft(4, '0')}"; - var newSpaceJson = newSpaceJsonTemplate.Replace("{space-name}", spaceName); - - var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); - var response = await httpClient.AsCustomer(customerId).PostAsync(postUrl, content); - var apiSpace = await response.ReadAsHydraResponseAsync(); - apiSpace.Name.Should().Be(spaceName); - } - - } - - [Fact] - public async Task DeleteSpace_Returns_Ok() - { - // Arrange - int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); - const string spaceJson = @"{ - ""name"": ""test space"" -}"; - - var content = new StringContent(spaceJson, Encoding.UTF8, "application/json"); - var response = await httpClient.AsCustomer().PostAsync($"/customers/{customerId}/spaces", content); - var space = await response.ReadAsHydraResponseAsync(); - - // Act - var deleteResponse = await httpClient.AsCustomer().DeleteAsync($"/customers/{customerId}/spaces/{space.ModelId}"); - - // Assert - deleteResponse.StatusCode.Should().Be(HttpStatusCode.NoContent); - var deletedSpace = await dbContext.Spaces.SingleOrDefaultAsync(s => s.Id == space.ModelId && s.Customer == customerId ); - deletedSpace.Should().BeNull(); - } - - [Fact] - public async Task DeleteSpace_NotFound_WhenCalledWithNonExistentSpace() - { - // Arrange & Act - int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); - var deleteResponse = await httpClient.AsCustomer().DeleteAsync($"/customers/{customerId}/spaces/456453"); - - // Assert - deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); - } - - [Fact] - public async Task DeleteSpace_InternalServerError_WhenDeletingSpaceWithImages() - { - // Arrange - int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); - const string spaceJson = @"{ - ""name"": ""test space"" -}"; - - var content = new StringContent(spaceJson, Encoding.UTF8, "application/json"); - var response = await httpClient.AsCustomer().PostAsync($"/customers/{customerId}/spaces", content); - var space = await response.ReadAsHydraResponseAsync(); - - var id = AssetId.FromString($"{customerId}/1/{space.ModelId}"); - await dbContext.Images.AddTestAsset(id, customer: customerId ?? 1, space: space.ModelId ?? 1); - await dbContext.SaveChangesAsync(); - - // Act - var deleteResponse = await httpClient.AsCustomer().DeleteAsync($"/customers/{customerId}/spaces/{space.ModelId}"); - - // Assert - deleteResponse.StatusCode.Should().Be(HttpStatusCode.InternalServerError); - } - +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using API.Client; +using API.Tests.Integration.Infrastructure; +using DLCS.Core.Types; +using DLCS.HydraModel; +using DLCS.Repository; +using DLCS.Repository.Entities; +using Hydra; +using Hydra.Collections; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; +using Test.Helpers.Integration; +using Test.Helpers.Integration.Infrastructure; + +namespace API.Tests.Integration; + +[Trait("Category", "Integration")] +[Collection(CollectionDefinitions.DatabaseCollection.CollectionName)] +public class SpaceTests : IClassFixture> +{ + private readonly DlcsContext dbContext; + private readonly HttpClient httpClient; + + public SpaceTests(DlcsDatabaseFixture dbFixture, ProtagonistAppFactory factory) + { + dbContext = dbFixture.DbContext; + httpClient = factory.ConfigureBasicAuthedIntegrationTestHttpClient(dbFixture, "API-Test"); + dbFixture.CleanUp(); + } + + [Fact] + public async Task Post_SimpleSpace_Creates_Space() + { + // arrange + int? customerId = 99; + var counter = await dbContext.EntityCounters.SingleAsync( + ec => ec.Customer == 99 && ec.Scope == "99" && ec.Type == "space"); + int expectedSpace = (int) counter.Next; + + const string newSpaceJson = @"{ + ""@type"": ""Space"", + ""name"": ""Test Space"" +}"; + // act + var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); + var postUrl = $"/customers/{customerId}/spaces"; + var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); + var apiSpace = await response.ReadAsHydraResponseAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + response.Headers.Location.PathAndQuery.Should().Be($"{postUrl}/{expectedSpace}"); + apiSpace.Should().NotBeNull(); + apiSpace.Name.Should().Be("Test Space"); + apiSpace.MaxUnauthorised.Should().Be(-1); + } + + [Fact] + public async Task Post_ComplexSpace_Creates_Space() + { + // arrange + int? customerId = 99; // await EnsureCustomerForSpaceTests("Post_ComplexSpace_Creates_Space"); + + const string newSpaceJson = @"{ + ""@type"": ""Space"", + ""name"": ""Test Complex Space"", + ""defaultRoles"": [""role1"", ""role2""], + ""defaultTags"": [""tag1"", ""tag2""], + ""maxUnauthorised"": 400 +}"; + // act + var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); + var postUrl = $"/customers/{customerId}/spaces"; + var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); + var apiSpace = await response.ReadAsHydraResponseAsync(); + + // assert + apiSpace.Should().NotBeNull(); + AssertSpace(apiSpace); + + // verify that we can re-obtain the space with GET + var newResponse = await httpClient.AsCustomer(customerId.Value).GetAsync(apiSpace.Id); + var reObtainedSpace = await newResponse.ReadAsHydraResponseAsync(); + AssertSpace(reObtainedSpace); + + void AssertSpace(Space space) + { + space.Name.Should().Be("Test Complex Space"); + space.DefaultRoles.Should().BeEquivalentTo("role1", "role2"); + space.DefaultTags.Should().BeEquivalentTo("tag1", "tag2"); + space.MaxUnauthorised.Should().Be(400); + } + } + + [Fact] + public async Task Create_Space_Updates_EntityCounters() + { + // After creating a space in Deliverator: + + // EXISTING COUNTER: was: + // {'Type': 'space', 'Scope': '2', 'Next': 35, 'Customer': 2} + // now: + // {'Type': 'space', 'Scope': '2', 'Next': 36, 'Customer': 2} + + // NEW COUNTER + // {'Type': 'space-images', 'Scope': '35', 'Next': 0, 'Customer': 2} + + // That last one seems wrong, should be Next = 1, not 0 + + int? customerId = await EnsureCustomerForSpaceTests(); + + var currentCounter = await dbContext.EntityCounters.SingleAsync( + ec => ec.Type == "space" && ec.Scope == customerId.ToString() && ec.Customer == customerId); + + var next = (int)currentCounter.Next; + const string newSpaceJson = @"{ + ""@type"": ""Space"", + ""name"": ""Entity Counter Test Space"" +}"; + + var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); + var postUrl = $"/customers/{customerId}/spaces"; + var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); + var apiSpace = await response.ReadAsHydraResponseAsync(); + + apiSpace.Id.Should().EndWith($"{postUrl}/{next}"); + currentCounter = await dbContext.EntityCounters.SingleAsync( + ec => ec.Type == "space" && ec.Scope == customerId.ToString() && ec.Customer == customerId); + currentCounter.Next.Should().Be(next + 1); + var spaceImageCounter = await dbContext.EntityCounters.SingleOrDefaultAsync( + ec => + ec.Type == KnownEntityCounters.SpaceImages && + ec.Customer == customerId.Value && + ec.Scope == next.ToString()); + spaceImageCounter.Should().NotBeNull(); + spaceImageCounter.Next.Should().Be(1); // Deliverator makes 0 here. But that doesn't feel right! + } + + [Fact] + public async Task GetSpaces_Returns_HydraCollection() + { + // arrange + int? customerId = await EnsureCustomerForSpaceTests("hydracollection_space"); + await EnsureSpaces(customerId.Value, 10); + var spacesUrl = $"/customers/{customerId.Value}/spaces"; + + // act + var response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var coll = await response.ReadAsHydraResponseAsync>(); + coll.Should().NotBeNull(); + coll.Type.Should().Be("Collection"); + coll.Members.Should().HaveCount(10); + coll.Members.Should().Contain(jo => jo["@id"].Value().EndsWith($"{spacesUrl}/1")); + coll.Members.Should().Contain(jo => jo["@id"].Value().EndsWith($"{spacesUrl}/10")); + } + + [Fact] + public async Task Paged_Requests_Return_Correct_Views() + { + // arrange + int? customerId = await EnsureCustomerForSpaceTests("hydracollection_space"); + await EnsureSpaces(customerId.Value, 55); + // set a pageSize of 10 + var spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10"; + + // act + var response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var coll = await response.ReadAsHydraResponseAsync>(); + coll.Should().NotBeNull(); + coll.Type.Should().Be("Collection"); + coll.Members.Should().HaveCount(10); + coll.PageSize.Should().Be(10); + coll.View.Should().NotBeNull(); + coll.View.Page.Should().Be(1); + coll.View.Previous.Should().BeNull(); + coll.View.Next.Should().Contain("page=2"); + coll.View.TotalPages.Should().Be(6); + int pageCounter = 1; + var view = coll.View; + while (view.Next != null) + { + var nextResp = await httpClient.AsCustomer(customerId.Value).GetAsync(view.Next); + var nextColl = await nextResp.ReadAsHydraResponseAsync>(); + view = nextColl.View; + view.Previous.Should().Contain("page=" + pageCounter); + pageCounter++; + if (pageCounter < 6) + { + nextColl.Members.Should().HaveCount(10); + view.Next.Should().Contain("page=" + (pageCounter + 1)); + } + else + { + nextColl.Members.Should().HaveCount(5); + view.Next.Should().BeNull(); + } + } + } + + [Fact] + public async Task Paged_Requests_Support_Ordering() + { + // arrange + int? customerId = await EnsureCustomerForSpaceTests("hydracollection_space"); + await EnsureSpaces(customerId.Value, 25); + + var spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderBy=name"; + + // act + var response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + + // assert + var coll = await response.ReadAsHydraResponseAsync>(); + coll.Members[0]["name"].ToString().Should().Be("Space 0001"); + coll.Members[1]["name"].ToString().Should().Be("Space 0002"); + + spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderByDescending=name"; + response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + coll = await response.ReadAsHydraResponseAsync>(); + coll.Members[0]["name"].ToString().Should().Be("Space 0025"); + coll.Members[1]["name"].ToString().Should().Be("Space 0024"); + + var nextPage = await httpClient.AsCustomer(customerId.Value).GetAsync(coll.View.Next); + coll = await nextPage.ReadAsHydraResponseAsync>(); + coll.Members[0]["name"].ToString().Should().Be("Space 0015"); + coll.Members[1]["name"].ToString().Should().Be("Space 0014"); + + // Add another space just to be sure we are testing created properly + const string newSpaceJson = @"{ + ""@type"": ""Space"", + ""name"": ""Aardvark space"" +}"; + var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); + var postUrl = $"/customers/{customerId}/spaces"; + await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); + + spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderBy=name"; + response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + coll = await response.ReadAsHydraResponseAsync>(); + coll.Members[0]["name"].ToString().Should().Be("Aardvark space"); + + spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderBy=created"; + response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + coll = await response.ReadAsHydraResponseAsync>(); + coll.Members[0]["name"].ToString().Should().Be("Space 0001"); + + spacesUrl = $"/customers/{customerId.Value}/spaces?pageSize=10&orderByDescending=created"; + response = await httpClient.AsCustomer(customerId.Value).GetAsync(spacesUrl); + coll = await response.ReadAsHydraResponseAsync>(); + coll.Members[0]["name"].ToString().Should().Be("Aardvark space"); + + } + + [Fact] + public async Task Patch_Space_Updates_Name() + { + int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); + await dbContext.Spaces.AddTestSpace(customerId.Value, 1, "Patch Space Before"); + await dbContext.SaveChangesAsync(); + + const string patchJson = @"{ +""@type"": ""Space"", +""name"": ""Patch Space After"" +}"; + var patchContent = new StringContent(patchJson, Encoding.UTF8, "application/json"); + var patchUrl = $"/customers/{customerId}/spaces/1"; + var patchResponse = await httpClient.AsCustomer(customerId.Value).PatchAsync(patchUrl, patchContent); + var patchedSpace = await patchResponse.ReadAsHydraResponseAsync(); + + patchResponse.StatusCode.Should().Be(HttpStatusCode.OK); + patchedSpace.Name.Should().Be("Patch Space After"); + } + + [Fact] + public async Task Patch_Space_Prevents_Name_Conflict() + { + int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Prevents_Name_Conflict"); + await dbContext.Spaces.AddTestSpace(customerId.Value, 1, "Patch Space Name 1"); + await dbContext.Spaces.AddTestSpace(customerId.Value, 2, "Patch Space Name 2"); + await dbContext.SaveChangesAsync(); + + const string patchJson = @"{ +""@type"": ""Space"", +""name"": ""Patch Space Name 2"" +}"; + var patchContent = new StringContent(patchJson, Encoding.UTF8, "application/json"); + var patchUrl = $"/customers/{customerId}/spaces/1"; + var patchResponse = await httpClient.AsCustomer(customerId.Value).PatchAsync(patchUrl, patchContent); + patchResponse.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [Fact] + public async Task Patch_Space_Leaves_Omitted_Fields_Intact() + { + // arrange + int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Leaves_Omitted_Fields_Intact"); + + const string newSpaceJson = @"{ + ""@type"": ""Space"", + ""name"": ""Patch Complex Space"", + ""defaultRoles"": [""role1"", ""role2""], + ""defaultTags"": [""tag1"", ""tag2""], + ""maxUnauthorised"": 400 + }"; + const string patchJson = @"{ + ""@type"": ""Space"", + ""name"": ""Patch Complex Space After"" + }"; + + // act + var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); + var postUrl = $"/customers/{customerId}/spaces"; + var response = await httpClient.AsCustomer(customerId.Value).PostAsync(postUrl, content); + var apiSpace = await response.ReadAsHydraResponseAsync(); + + // assert + var patchContent = new StringContent(patchJson, Encoding.UTF8, "application/json"); + var patchResponse = await httpClient.AsCustomer(customerId.Value).PatchAsync(apiSpace.Id, patchContent); + var patchedSpace = await patchResponse.ReadAsHydraResponseAsync(); + + patchedSpace.Name.Should().Be("Patch Complex Space After"); + patchedSpace.DefaultRoles.Should().BeEquivalentTo("role1", "role2"); + patchedSpace.DefaultTags.Should().BeEquivalentTo("tag1", "tag2"); + patchedSpace.MaxUnauthorised.Should().Be(400); + } + + private async Task EnsureCustomerForSpaceTests(string customerName = "space-test-customer") + { + var spaceTestCustomer = await dbContext.Customers.SingleOrDefaultAsync(c => c.Name == customerName); + + if (spaceTestCustomer != null) + { + return spaceTestCustomer.Id; + } + + string spaceTestCustomerJson = $@"{{ + ""@type"": ""Customer"", + ""name"": ""{customerName}"", + ""displayName"": ""Display - {customerName}"" +}}"; + + var content = new StringContent(spaceTestCustomerJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsAdmin().PostAsync("/customers", content); + var apiCustomer = await response.ReadAsHydraResponseAsync(); + return apiCustomer?.Id.GetLastPathElementAsInt(); + } + + /// + /// Create lots of spaces to test paging + /// + /// + private async Task EnsureSpaces(int customerId, int numberOfSpaces) + { + var seed = DateTime.Now.Ticks.ToString(); + const string newSpaceJsonTemplate = @"{ + ""@type"": ""Space"", + ""name"": ""{space-name}"" +}"; + var postUrl = $"/customers/{customerId}/spaces"; + + for (int i = 1; i <= numberOfSpaces; i++) + { + var spaceName = $"Space {i.ToString().PadLeft(4, '0')}"; + var newSpaceJson = newSpaceJsonTemplate.Replace("{space-name}", spaceName); + + var content = new StringContent(newSpaceJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer(customerId).PostAsync(postUrl, content); + var apiSpace = await response.ReadAsHydraResponseAsync(); + apiSpace.Name.Should().Be(spaceName); + } + + } + + [Fact] + public async Task DeleteSpace_Returns_Ok() + { + // Arrange + int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); + const string spaceJson = @"{ + ""name"": ""test space"" +}"; + + var content = new StringContent(spaceJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer().PostAsync($"/customers/{customerId}/spaces", content); + var space = await response.ReadAsHydraResponseAsync(); + + // Act + var deleteResponse = await httpClient.AsCustomer().DeleteAsync($"/customers/{customerId}/spaces/{space.ModelId}"); + + // Assert + deleteResponse.StatusCode.Should().Be(HttpStatusCode.NoContent); + var deletedSpace = await dbContext.Spaces.SingleOrDefaultAsync(s => s.Id == space.ModelId && s.Customer == customerId ); + deletedSpace.Should().BeNull(); + } + + [Fact] + public async Task DeleteSpace_NotFound_WhenCalledWithNonExistentSpace() + { + // Arrange & Act + int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); + var deleteResponse = await httpClient.AsCustomer().DeleteAsync($"/customers/{customerId}/spaces/456453"); + + // Assert + deleteResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Fact] + public async Task DeleteSpace_InternalServerError_WhenDeletingSpaceWithImages() + { + // Arrange + int? customerId = await EnsureCustomerForSpaceTests("Patch_Space_Updates_Name"); + const string spaceJson = @"{ + ""name"": ""test space"" +}"; + + var content = new StringContent(spaceJson, Encoding.UTF8, "application/json"); + var response = await httpClient.AsCustomer().PostAsync($"/customers/{customerId}/spaces", content); + var space = await response.ReadAsHydraResponseAsync(); + + var id = AssetId.FromString($"{customerId}/1/{space.ModelId}"); + await dbContext.Images.AddTestAsset(id, customer: customerId ?? 1, space: space.ModelId ?? 1); + await dbContext.SaveChangesAsync(); + + // Act + var deleteResponse = await httpClient.AsCustomer().DeleteAsync($"/customers/{customerId}/spaces/{space.ModelId}"); + + // Assert + deleteResponse.StatusCode.Should().Be(HttpStatusCode.InternalServerError); + } + } \ No newline at end of file diff --git a/src/protagonist/API/Infrastructure/HydraController.cs b/src/protagonist/API/Infrastructure/HydraController.cs index 6e765bd2a..e90e054dc 100644 --- a/src/protagonist/API/Infrastructure/HydraController.cs +++ b/src/protagonist/API/Infrastructure/HydraController.cs @@ -1,344 +1,344 @@ -using System.Collections.Generic; -using API.Converters; -using API.Exceptions; -using API.Features.Image.Requests; -using API.Infrastructure.Requests; -using API.Settings; -using DLCS.Core; -using DLCS.HydraModel; -using DLCS.Model.Page; -using DLCS.Web.Requests; -using Hydra.Collections; -using Hydra.Model; -using MediatR; -using Microsoft.AspNetCore.Mvc; - -namespace API.Infrastructure; - -/// -/// Base class for DLCS API Controllers that return Hydra responses -/// -public abstract class HydraController : Controller -{ - /// - /// API Settings available to derived controller classes - /// - protected readonly ApiSettings Settings; - - protected readonly IMediator Mediator; - - /// - protected HydraController(ApiSettings settings, IMediator mediator) - { - Settings = settings; - Mediator = mediator; - } - - /// - /// Used by derived controllers to construct correct fully qualified URLs in returned Hydra objects. - /// - /// - protected UrlRoots GetUrlRoots() - { - return new UrlRoots - { - BaseUrl = Request.GetBaseUrl(), - ResourceRoot = Settings.DLCS.ResourceRoot.ToString() - }; - } - - /// - /// Handle an upsert request - this takes a IRequest which returns a ModifyEntityResult{T}. - /// The request is sent and result is transformed to an http hydra result. - /// - /// IRequest to modify data - /// Delegate to transform returned entity to a Hydra representation - /// The value for . - /// - /// The value for . In some instances this will be prepended to the actual error name. - /// e.g. errorTitle + ": Conflict" - /// - /// Current cancellation token - /// Type of entity being upserted - /// - /// ActionResult generated from ModifyEntityResult. This will be the Hydra model + 200/201 on success. Or a Hydra - /// error and appropriate status code if failed. - /// - protected async Task HandleUpsert( - IRequest> request, - Func hydraBuilder, - string? instance = null, - string? errorTitle = "Operation failed", - CancellationToken cancellationToken = default) - where T : class - { - return await HandleHydraRequest(async () => - { - var result = await Mediator.Send(request, cancellationToken); - - return this.ModifyResultToHttpResult(result, hydraBuilder, instance, errorTitle); - }, errorTitle); - } - - /// - /// Handles a deletion - /// - /// The request/response to be sent through Mediatr - /// The title of the error - /// Current cancellation token - /// The type of entity that was to be deleted - /// ActionResult generated from DeleteResult. This will be 204 on success. Or a Hydra - /// error and appropriate status code if failed. - protected async Task HandleDelete( - IRequest<(DeleteResult deleteReuslt, string errorMessage)> request, - string? errorTitle = "Delete failed", - CancellationToken cancellationToken = default) - where T : class - { - return await HandleHydraRequest(async () => - { - var result = await Mediator.Send(request, cancellationToken); - - return result.deleteReuslt switch - { - DeleteResult.NotFound => this.HydraNotFound(), - DeleteResult.Error => this.HydraProblem(result.errorMessage, null, 500, - "Delete failed"), - _ => NoContent() - }; - }, errorTitle); - } - - /// - /// Handle a GET request - this takes a IRequest which returns a FetchEntityResult{T}. - /// The request is sent and result is transformed to an http hydra result. - /// - /// IRequest to fetch data - /// Delegate to transform returned entity to a Hydra representation - /// The value for . - /// - /// The value for . In some instances this will be prepended to the actual error name. - /// e.g. errorTitle + ": Conflict" - /// - /// Current cancellation token - /// Type of entity being fetched - /// - /// ActionResult generated from FetchEntityResult. This will be the Hydra model + 200 on success. Or a Hydra - /// error and appropriate status code if failed. - /// - protected async Task HandleFetch( - IRequest> request, - Func hydraBuilder, - string? instance = null, - string? errorTitle = "Fetch failed", - CancellationToken cancellationToken = default) - where T : class - { - return await HandleHydraRequest(async () => - { - var result = await Mediator.Send(request, cancellationToken); - - return this.FetchResultToHttpResult(result, instance, errorTitle, hydraBuilder); - }, errorTitle); - } - - /// - /// Handle a GET request that returns a page of assets. - /// This takes a IRequest which returns a FetchEntityResult{PageOf{T}} and inherits from IPagedRequest - /// Prior to making request the Page and PageSize properties are set from query parameters. - /// The request is sent and result is transformed to HydraCollection. - /// - /// IRequest to fetch data - /// Delegate to transform each returned entity to a Hydra representation - /// The value for . - /// - /// The value for . In some instances this will be prepended to the actual error name. - /// e.g. errorTitle + ": Conflict" - /// - /// Current cancellation token - /// Type of db entity being fetched - /// Type of mediatr request being page - /// Hydra type for each member - /// - /// ActionResult generated from FetchEntityResult. This will be the HydraCollection + 200 on success. Or a Hydra - /// error and appropriate status code if failed. - /// - protected async Task HandlePagedFetch( - TRequest request, - Func hydraBuilder, - string? instance = null, - string? errorTitle = "Fetch failed", - CancellationToken cancellationToken = default) - where TEntity : class - where TRequest : IRequest>>, IPagedRequest - where THydra : DlcsResource - { - return await HandleHydraRequest(async () => - { - SetPaging(request); - if (request is IOrderableRequest orderableRequest) - { - SetOrderBy(orderableRequest); - } - - var result = await Mediator.Send(request, cancellationToken); - - return this.FetchResultToHttpResult( - result, - instance, - errorTitle, pageOf => - { - var collection = new HydraCollection - { - WithContext = true, - Members = pageOf.Entities.Select(b => hydraBuilder(b)).ToArray(), - TotalItems = pageOf.Total, - PageSize = pageOf.PageSize, - Id = Request.GetJsonLdId() - }; - PartialCollectionView.AddPaging(collection, new PartialCollectionViewPagingValues - { - Page = pageOf.Page, PageSize = pageOf.PageSize, - FurtherParameters = GetFurtherPageLinkParameters(request) - }); - return collection; - }); - }, errorTitle); - } - - private List>? GetFurtherPageLinkParameters(IPagedRequest pagedRequest) - { - List>? furtherParameters = null; - - if (pagedRequest is IAssetFilterableRequest assetFilterableRequest) - { - if (assetFilterableRequest.AssetFilter != null) - { - var imageQuery = assetFilterableRequest.AssetFilter.ToImageQuery(); - furtherParameters ??= new List>(); - furtherParameters.Add(new KeyValuePair("q", imageQuery.ToQueryParam())); - } - } - - // Add any other parameters we want to pass through here - - return furtherParameters; - } - - /// - /// Handle a request that returns a non-paged list of assets. - /// This takes a IRequest which returns a FetchEntityResult{IReadOnlyCollection{T}} - /// The request is sent and result is transformed to HydraCollection. - /// - /// IRequest to fetch data - /// Delegate to transform each returned entity to a Hydra representation - /// The value for . - /// - /// The value for . In some instances this will be prepended to the actual error name. - /// e.g. errorTitle + ": Conflict" - /// - /// Current cancellation token - /// Type of db entity being fetched - /// Type of mediatr request being page - /// Hydra type for each member - /// - /// ActionResult generated from FetchEntityResult. This will be the HydraCollection + 200 on success. Or a Hydra - /// error and appropriate status code if failed. - /// - protected async Task HandleListFetch( - TRequest request, - Func hydraBuilder, - string? instance = null, - string? errorTitle = "Fetch failed", - CancellationToken cancellationToken = default) - where TRequest : IRequest>> - where THydra : DlcsResource - { - return await HandleHydraRequest(async () => - { - var result = await Mediator.Send(request, cancellationToken); - - return this.FetchResultToHttpResult( - result, - instance, - errorTitle, results => - { - return new HydraCollection - { - WithContext = true, - Members = results.Select(b => hydraBuilder(b)).ToArray(), - TotalItems = results.Count, - PageSize = results.Count, - Id = Request.GetJsonLdId() - }; - }); - }, errorTitle); - } - - /// - /// Make a request and handle exceptions, converting to a HydraProblem - /// - protected async Task HandleHydraRequest(Func> handler, - string? errorTitle = "Request failed") - { - try - { - return await handler(); - } - catch (APIException apiEx) - { - return this.HydraProblem(apiEx.Message, null, apiEx.StatusCode ?? 500, apiEx.Label); - } - catch (Exception ex) - { - return this.HydraProblem(ex.Message, null, 500, errorTitle); - } - } - - /// - /// Set Page and PageSize properties on specified request, reading values from query params. - /// Page is set from ?page - if provided values is <= 0 it's defaulted to 0 - /// PageSize is set from ?pageSize - if provided values is <= 0 or > 500 it's defaulted to PageSize setting - /// - /// Request object to update - protected void SetPaging(IPagedRequest pagedrequest) - { - if (Request.Query.TryGetValue("page", out var page)) - { - if (int.TryParse(page, out var parsedPage)) - { - pagedrequest.Page = parsedPage; - } - } - - if (Request.Query.TryGetValue("pageSize", out var pageSize)) - { - if (int.TryParse(pageSize, out var parsedPageSize)) - { - pagedrequest.PageSize = parsedPageSize; - } - } - - if (pagedrequest.PageSize is <= 0 or > 500) pagedrequest.PageSize = Settings.PageSize; - if (pagedrequest.Page <= 0) pagedrequest.Page = 1; - } - - /// - /// Set Field and Descending properties on specified request, reading properties from query params. - /// Field is from ?orderBy or ?orderByDescending. Descending true if latter, false if former. - /// - /// Request object to update - protected void SetOrderBy(IOrderableRequest orderableRequest) - { - if (Request.Query.TryGetValue("orderBy", out var orderBy)) - { - orderableRequest.Field = orderBy; - orderableRequest.Descending = false; - } - else if (Request.Query.TryGetValue("orderByDescending", out var orderByDescending)) - { - orderableRequest.Field = orderByDescending; - orderableRequest.Descending = true; - } - } +using System.Collections.Generic; +using API.Converters; +using API.Exceptions; +using API.Features.Image.Requests; +using API.Infrastructure.Requests; +using API.Settings; +using DLCS.Core; +using DLCS.HydraModel; +using DLCS.Model.Page; +using DLCS.Web.Requests; +using Hydra.Collections; +using Hydra.Model; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace API.Infrastructure; + +/// +/// Base class for DLCS API Controllers that return Hydra responses +/// +public abstract class HydraController : Controller +{ + /// + /// API Settings available to derived controller classes + /// + protected readonly ApiSettings Settings; + + protected readonly IMediator Mediator; + + /// + protected HydraController(ApiSettings settings, IMediator mediator) + { + Settings = settings; + Mediator = mediator; + } + + /// + /// Used by derived controllers to construct correct fully qualified URLs in returned Hydra objects. + /// + /// + protected UrlRoots GetUrlRoots() + { + return new UrlRoots + { + BaseUrl = Request.GetBaseUrl(), + ResourceRoot = Settings.DLCS.ResourceRoot.ToString() + }; + } + + /// + /// Handle an upsert request - this takes a IRequest which returns a ModifyEntityResult{T}. + /// The request is sent and result is transformed to an http hydra result. + /// + /// IRequest to modify data + /// Delegate to transform returned entity to a Hydra representation + /// The value for . + /// + /// The value for . In some instances this will be prepended to the actual error name. + /// e.g. errorTitle + ": Conflict" + /// + /// Current cancellation token + /// Type of entity being upserted + /// + /// ActionResult generated from ModifyEntityResult. This will be the Hydra model + 200/201 on success. Or a Hydra + /// error and appropriate status code if failed. + /// + protected async Task HandleUpsert( + IRequest> request, + Func hydraBuilder, + string? instance = null, + string? errorTitle = "Operation failed", + CancellationToken cancellationToken = default) + where T : class + { + return await HandleHydraRequest(async () => + { + var result = await Mediator.Send(request, cancellationToken); + + return this.ModifyResultToHttpResult(result, hydraBuilder, instance, errorTitle); + }, errorTitle); + } + + /// + /// Handles a deletion + /// + /// The request/response to be sent through Mediatr + /// The title of the error + /// Current cancellation token + /// The type of entity that was to be deleted + /// ActionResult generated from DeleteResult. This will be 204 on success. Or a Hydra + /// error and appropriate status code if failed. + protected async Task HandleDelete( + IRequest<(DeleteResult deleteReuslt, string errorMessage)> request, + string? errorTitle = "Delete failed", + CancellationToken cancellationToken = default) + where T : class + { + return await HandleHydraRequest(async () => + { + var result = await Mediator.Send(request, cancellationToken); + + return result.deleteReuslt switch + { + DeleteResult.NotFound => this.HydraNotFound(), + DeleteResult.Error => this.HydraProblem(result.errorMessage, null, 500, + "Delete failed"), + _ => NoContent() + }; + }, errorTitle); + } + + /// + /// Handle a GET request - this takes a IRequest which returns a FetchEntityResult{T}. + /// The request is sent and result is transformed to an http hydra result. + /// + /// IRequest to fetch data + /// Delegate to transform returned entity to a Hydra representation + /// The value for . + /// + /// The value for . In some instances this will be prepended to the actual error name. + /// e.g. errorTitle + ": Conflict" + /// + /// Current cancellation token + /// Type of entity being fetched + /// + /// ActionResult generated from FetchEntityResult. This will be the Hydra model + 200 on success. Or a Hydra + /// error and appropriate status code if failed. + /// + protected async Task HandleFetch( + IRequest> request, + Func hydraBuilder, + string? instance = null, + string? errorTitle = "Fetch failed", + CancellationToken cancellationToken = default) + where T : class + { + return await HandleHydraRequest(async () => + { + var result = await Mediator.Send(request, cancellationToken); + + return this.FetchResultToHttpResult(result, instance, errorTitle, hydraBuilder); + }, errorTitle); + } + + /// + /// Handle a GET request that returns a page of assets. + /// This takes a IRequest which returns a FetchEntityResult{PageOf{T}} and inherits from IPagedRequest + /// Prior to making request the Page and PageSize properties are set from query parameters. + /// The request is sent and result is transformed to HydraCollection. + /// + /// IRequest to fetch data + /// Delegate to transform each returned entity to a Hydra representation + /// The value for . + /// + /// The value for . In some instances this will be prepended to the actual error name. + /// e.g. errorTitle + ": Conflict" + /// + /// Current cancellation token + /// Type of db entity being fetched + /// Type of mediatr request being page + /// Hydra type for each member + /// + /// ActionResult generated from FetchEntityResult. This will be the HydraCollection + 200 on success. Or a Hydra + /// error and appropriate status code if failed. + /// + protected async Task HandlePagedFetch( + TRequest request, + Func hydraBuilder, + string? instance = null, + string? errorTitle = "Fetch failed", + CancellationToken cancellationToken = default) + where TEntity : class + where TRequest : IRequest>>, IPagedRequest + where THydra : DlcsResource + { + return await HandleHydraRequest(async () => + { + SetPaging(request); + if (request is IOrderableRequest orderableRequest) + { + SetOrderBy(orderableRequest); + } + + var result = await Mediator.Send(request, cancellationToken); + + return this.FetchResultToHttpResult( + result, + instance, + errorTitle, pageOf => + { + var collection = new HydraCollection + { + WithContext = true, + Members = pageOf.Entities.Select(b => hydraBuilder(b)).ToArray(), + TotalItems = pageOf.Total, + PageSize = pageOf.PageSize, + Id = Request.GetJsonLdId() + }; + PartialCollectionView.AddPaging(collection, new PartialCollectionViewPagingValues + { + Page = pageOf.Page, PageSize = pageOf.PageSize, + FurtherParameters = GetFurtherPageLinkParameters(request) + }); + return collection; + }); + }, errorTitle); + } + + private List>? GetFurtherPageLinkParameters(IPagedRequest pagedRequest) + { + List>? furtherParameters = null; + + if (pagedRequest is IAssetFilterableRequest assetFilterableRequest) + { + if (assetFilterableRequest.AssetFilter != null) + { + var imageQuery = assetFilterableRequest.AssetFilter.ToImageQuery(); + furtherParameters ??= new List>(); + furtherParameters.Add(new KeyValuePair("q", imageQuery.ToQueryParam())); + } + } + + // Add any other parameters we want to pass through here + + return furtherParameters; + } + + /// + /// Handle a request that returns a non-paged list of assets. + /// This takes a IRequest which returns a FetchEntityResult{IReadOnlyCollection{T}} + /// The request is sent and result is transformed to HydraCollection. + /// + /// IRequest to fetch data + /// Delegate to transform each returned entity to a Hydra representation + /// The value for . + /// + /// The value for . In some instances this will be prepended to the actual error name. + /// e.g. errorTitle + ": Conflict" + /// + /// Current cancellation token + /// Type of db entity being fetched + /// Type of mediatr request being page + /// Hydra type for each member + /// + /// ActionResult generated from FetchEntityResult. This will be the HydraCollection + 200 on success. Or a Hydra + /// error and appropriate status code if failed. + /// + protected async Task HandleListFetch( + TRequest request, + Func hydraBuilder, + string? instance = null, + string? errorTitle = "Fetch failed", + CancellationToken cancellationToken = default) + where TRequest : IRequest>> + where THydra : DlcsResource + { + return await HandleHydraRequest(async () => + { + var result = await Mediator.Send(request, cancellationToken); + + return this.FetchResultToHttpResult( + result, + instance, + errorTitle, results => + { + return new HydraCollection + { + WithContext = true, + Members = results.Select(b => hydraBuilder(b)).ToArray(), + TotalItems = results.Count, + PageSize = results.Count, + Id = Request.GetJsonLdId() + }; + }); + }, errorTitle); + } + + /// + /// Make a request and handle exceptions, converting to a HydraProblem + /// + protected async Task HandleHydraRequest(Func> handler, + string? errorTitle = "Request failed") + { + try + { + return await handler(); + } + catch (APIException apiEx) + { + return this.HydraProblem(apiEx.Message, null, apiEx.StatusCode ?? 500, apiEx.Label); + } + catch (Exception ex) + { + return this.HydraProblem(ex.Message, null, 500, errorTitle); + } + } + + /// + /// Set Page and PageSize properties on specified request, reading values from query params. + /// Page is set from ?page - if provided values is <= 0 it's defaulted to 0 + /// PageSize is set from ?pageSize - if provided values is <= 0 or > 500 it's defaulted to PageSize setting + /// + /// Request object to update + protected void SetPaging(IPagedRequest pagedrequest) + { + if (Request.Query.TryGetValue("page", out var page)) + { + if (int.TryParse(page, out var parsedPage)) + { + pagedrequest.Page = parsedPage; + } + } + + if (Request.Query.TryGetValue("pageSize", out var pageSize)) + { + if (int.TryParse(pageSize, out var parsedPageSize)) + { + pagedrequest.PageSize = parsedPageSize; + } + } + + if (pagedrequest.PageSize is <= 0 or > 500) pagedrequest.PageSize = Settings.PageSize; + if (pagedrequest.Page <= 0) pagedrequest.Page = 1; + } + + /// + /// Set Field and Descending properties on specified request, reading properties from query params. + /// Field is from ?orderBy or ?orderByDescending. Descending true if latter, false if former. + /// + /// Request object to update + protected void SetOrderBy(IOrderableRequest orderableRequest) + { + if (Request.Query.TryGetValue("orderBy", out var orderBy)) + { + orderableRequest.Field = orderBy; + orderableRequest.Descending = false; + } + else if (Request.Query.TryGetValue("orderByDescending", out var orderByDescending)) + { + orderableRequest.Field = orderByDescending; + orderableRequest.Descending = true; + } + } } \ No newline at end of file diff --git a/src/protagonist/DLCS.Model/Spaces/ISpaceRepository.cs b/src/protagonist/DLCS.Model/Spaces/ISpaceRepository.cs index 3bbb1e686..7bc961f7a 100644 --- a/src/protagonist/DLCS.Model/Spaces/ISpaceRepository.cs +++ b/src/protagonist/DLCS.Model/Spaces/ISpaceRepository.cs @@ -1,27 +1,27 @@ -using System.Threading; -using System.Threading.Tasks; -using DLCS.Core; - -namespace DLCS.Model.Spaces; - -public interface ISpaceRepository -{ - Task GetImageCountForSpace(int customerId, int spaceId); - - Task GetSpace(int customerId, int spaceId, CancellationToken cancellationToken); - - Task GetSpace(int customerId, int spaceId, bool noCache, CancellationToken cancellationToken); - - Task GetSpace(int customerId, string name, CancellationToken cancellationToken); - - Task CreateSpace(int customer, string name, string? imageBucket, string[]? tags, string[]? roles, - int? maxUnauthorised, CancellationToken cancellationToken); - - Task GetPageOfSpaces(int customerId, int page, int pageSize, string orderBy, bool descending, - CancellationToken cancellationToken); - - Task PatchSpace(int customerId, int spaceId, string? name, int? maxUnauthorised, string[]? tags, - string[]? roles, CancellationToken cancellationToken); - - Task<(DeleteResult, string)> DeleteSpace(int customerId, int spaceId, CancellationToken cancellationToken); +using System.Threading; +using System.Threading.Tasks; +using DLCS.Core; + +namespace DLCS.Model.Spaces; + +public interface ISpaceRepository +{ + Task GetImageCountForSpace(int customerId, int spaceId); + + Task GetSpace(int customerId, int spaceId, CancellationToken cancellationToken); + + Task GetSpace(int customerId, int spaceId, bool noCache, CancellationToken cancellationToken); + + Task GetSpace(int customerId, string name, CancellationToken cancellationToken); + + Task CreateSpace(int customer, string name, string? imageBucket, string[]? tags, string[]? roles, + int? maxUnauthorised, CancellationToken cancellationToken); + + Task GetPageOfSpaces(int customerId, int page, int pageSize, string orderBy, bool descending, + CancellationToken cancellationToken); + + Task PatchSpace(int customerId, int spaceId, string? name, int? maxUnauthorised, string[]? tags, + string[]? roles, CancellationToken cancellationToken); + + Task<(DeleteResult, string)> DeleteSpace(int customerId, int spaceId, CancellationToken cancellationToken); } \ No newline at end of file