From c8180d508b6c088d6e4363f4c96a6bc4a3f18677 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Wed, 1 May 2024 10:54:26 +0200 Subject: [PATCH] V14: Test new content and media list view service (#16193) * Fix operation status msg * Cleanup * Removing unused status * Adding WithConfigurationEditor on DataEditorBuilder to be able to create new list views with custom configuration * Adding list view service tests for content and media * Adding list view service tests base * Consistency * Clean up * More cleanup --------- Co-authored-by: Bjarke Berg --- .../ContentCollectionControllerBase.cs | 4 - .../Content/ContentControllerBase.cs | 2 +- .../ContentCollectionOperationStatus.cs | 1 - .../Implement/ContentListViewService.cs | 1 - .../Implement/MediaListViewService.cs | 1 - .../Builders/DataEditorBuilder.cs | 9 +- .../Services/ContentListViewServiceTests.cs | 1014 +++++++++++++++++ .../ContentListViewServiceTestsBase.cs | 53 + .../Services/MediaListViewServiceTests.cs | 244 ++++ 9 files changed, 1320 insertions(+), 9 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTestsBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaListViewServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs index 8d55e09b57..95f4bb5498 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs @@ -68,10 +68,6 @@ public abstract class ContentCollectionControllerBase new BadRequestObjectResult(problemDetailsBuilder - .WithTitle("Data type id does not represent the configured collection") - .WithDetail($"The specified data type is not the configured collection for the given {type} item") - .Build()), ContentCollectionOperationStatus.DataTypeNotContentProperty => new BadRequestObjectResult(problemDetailsBuilder .WithTitle($"Data type id is not a {type} property") .WithDetail($"The specified data type is not part of the {type} properties") diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs index 66bf93ca20..a2277820bf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs @@ -17,7 +17,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase .WithDetail("A notification handler prevented the content operation.") .Build()), ContentEditingOperationStatus.ContentTypeNotFound => NotFound(problemDetailsBuilder - .WithTitle("The requested content could not be found") + .WithTitle("The requested content type could not be found") .Build()), ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch => BadRequest(problemDetailsBuilder .WithTitle("Content type culture variance mismatch") diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentCollectionOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentCollectionOperationStatus.cs index 9be4348bec..90980a2f35 100644 --- a/src/Umbraco.Core/Services/OperationStatus/ContentCollectionOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/ContentCollectionOperationStatus.cs @@ -8,7 +8,6 @@ public enum ContentCollectionOperationStatus ContentNotFound, ContentTypeNotFound, DataTypeNotCollection, - DataTypeNotContentCollection, DataTypeNotContentProperty, DataTypeNotFound, DataTypeWithoutContentType, diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs index d151b85c82..ecbc6e4003 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentListViewService.cs @@ -5,7 +5,6 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.AuthorizationStatus; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs b/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs index 11025485ae..42b6bbf421 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MediaListViewService.cs @@ -4,7 +4,6 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.AuthorizationStatus; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/tests/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs b/tests/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs index d29eea8bcf..76b6774190 100644 --- a/tests/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/DataEditorBuilder.cs @@ -20,6 +20,7 @@ public class DataEditorBuilder private string _alias; private IDictionary _defaultConfiguration; private string _name; + private ConfigurationEditor? _configurationEditor; public DataEditorBuilder(TParent parentBuilder) : base(parentBuilder) @@ -46,6 +47,12 @@ public class DataEditorBuilder return this; } + public DataEditorBuilder WithConfigurationEditor(ConfigurationEditor configurationEditor) + { + _configurationEditor = configurationEditor; + return this; + } + public ConfigurationEditorBuilder> AddExplicitConfigurationEditorBuilder() => _explicitConfigurationEditorBuilder; @@ -58,7 +65,7 @@ public class DataEditorBuilder var alias = _alias ?? name.ToCamelCase(); var defaultConfiguration = _defaultConfiguration ?? new Dictionary(); - var explicitConfigurationEditor = _explicitConfigurationEditorBuilder.Build(); + var explicitConfigurationEditor = _configurationEditor ?? _explicitConfigurationEditorBuilder.Build(); var explicitValueEditor = _explicitValueEditorBuilder.Build(); return new DataEditor( diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs new file mode 100644 index 0000000000..68a8ee0e11 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTests.cs @@ -0,0 +1,1014 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +public class ContentListViewServiceTests : ContentListViewServiceTestsBase +{ + private static readonly Guid CustomListViewKey = new("AD8E2AAF-6801-408A-8CCF-EFAC0312729B"); + + private IContentListViewService ContentListViewService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private ILanguageService LanguageService => GetRequiredService(); + + private IUser SuperUser { get; set; } + + [SetUp] + public async Task Setup() + => SuperUser = await GetSuperUser(); + + [Test] + public async Task Cannot_Get_List_View_Items_Of_Non_Existing_Content() + { + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + Guid.NewGuid(), + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.ContentNotFound, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Cannot_Get_List_View_Items_When_Content_Is_Not_Configured_As_List_View() + { + // Arrange + var contentType = ContentTypeBuilder.CreateSimpleContentType(); + contentType.AllowedAsRoot = true; + ContentTypeService.Save(contentType); + + var createModel = new ContentCreateModel + { + ContentTypeKey = contentType.Key, + InvariantName = "Page", + }; + + var createResult = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + createResult.Result.Content.Key, + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + // Assert the content type is not configured as list view + Assert.IsNull(createResult.Result.Content.ContentType.ListView); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.ContentNotCollection, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Can_Get_List_View_Items_By_Key() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + var descendants = ContentService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + // Assert the content type is configured as list view + Assert.IsNotNull(root.ContentType.ListView); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel collectionItemsResult = result.Result.Items; + + Assert.Multiple(() => + { + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + }); + } + + [Test] + public async Task Can_Get_Associated_List_View_Configuration_From_Content_Type() + { + // Arrange + var listViewConfiguration = new Dictionary + { + ["includeProperties"] = new[] + { + new Dictionary { { "alias", "contentTypeAlias" }, { "isSystem", true } }, + new Dictionary { { "alias", "createDate" }, { "isSystem", true } }, + new Dictionary { { "alias", "creator" }, { "isSystem", true } }, + new Dictionary { { "alias", "published" }, { "isSystem", true } }, + new Dictionary { { "alias", "sortOrder" }, { "isSystem", true } }, + new Dictionary { { "alias", "updateDate" }, { "isSystem", true } }, + new Dictionary { { "alias", "updater" }, { "isSystem", true } }, + }, + }; + + var customListView = await CreateCustomListViewDataType(listViewConfiguration); + + var root = await CreateRootContentWithFiveChildrenAsListViewItems(customListView.Key); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + + await AssertListViewConfiguration(result.Result.ListViewConfiguration, customListView.Key); + }); + } + + [Test] + public async Task Cannot_Get_Items_With_List_View_Configuration_From_Content_Type_When_List_View_Gets_Deleted() + { + // Arrange + var dataType = new DataTypeBuilder() + .WithId(0) + .WithName("Custom list view") + .WithDatabaseType(ValueStorageType.Nvarchar) + .AddEditor() + .WithAlias(Constants.PropertyEditors.Aliases.ListView) + .WithConfigurationEditor(new ListViewConfigurationEditor(IOHelper)) + .Done() + .Build(); + + var dataTypeCreateResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + + // Guard Assert + Assert.Multiple(() => + { + Assert.IsTrue(dataTypeCreateResult.Success); + Assert.IsTrue(dataTypeCreateResult.Result.ConfigurationObject is ListViewConfiguration); + }); + + var listViewKey = dataTypeCreateResult.Result.Key; + var root = await CreateRootContentWithFiveChildrenAsListViewItems(listViewKey); + + var dataTypeDeleteResult = await DataTypeService.DeleteAsync(listViewKey, Constants.Security.SuperUserKey); + Assert.IsTrue(dataTypeDeleteResult.Success); // Guard Assert + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.CollectionNotFound, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Cannot_Get_Items_With_List_View_Configuration_From_Content_Type_When_No_Configured_List_View_Properties() + { + // Arrange + // Overwrite default IncludeProperties added by ListViewConfiguration + var listViewConfiguration = new Dictionary + { + ["includeProperties"] = Array.Empty>(), + }; + + var configurationEditor = new ListViewConfigurationEditor(IOHelper) { DefaultConfiguration = listViewConfiguration }; + + var dataType = new DataTypeBuilder() + .WithId(0) + .WithName("Custom list view") + .WithDatabaseType(ValueStorageType.Nvarchar) + .AddEditor() + .WithAlias(Constants.PropertyEditors.Aliases.ListView) + .WithConfigurationEditor(configurationEditor) + .Done() + .Build(); + + var dataTypeCreateResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + + // Guard Assert + Assert.Multiple(() => + { + Assert.IsTrue(dataTypeCreateResult.Success); + Assert.IsTrue(dataTypeCreateResult.Result.ConfigurationObject is ListViewConfiguration); + }); + + var root = await CreateRootContentWithFiveChildrenAsListViewItems(dataTypeCreateResult.Result.Key); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.MissingPropertiesInCollectionConfiguration, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Can_Get_Items_For_List_View_Property_On_Content() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAndListViewProperty(); + var descendants = ContentService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + CustomListViewKey, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + // Assert the content type is not configured as list view + Assert.IsNull(root.ContentType.ListView); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel collectionItemsResult = result.Result.Items; + + Assert.Multiple(() => + { + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + }); + } + + [Test] + public async Task Can_Get_Associated_List_View_Configuration_From_List_View_Property_On_Content() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAndListViewProperty(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + CustomListViewKey, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + + await AssertListViewConfiguration(result.Result.ListViewConfiguration, CustomListViewKey); + }); + + } + + [Test] + public async Task Cannot_Get_Items_For_List_View_Property_On_Content_When_Data_Type_Is_Not_Content_Type_Property() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAndListViewProperty(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + Constants.DataTypes.Guids.ListViewContentGuid, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + // Assert the content type is not configured as list view + Assert.IsNull(root.ContentType.ListView); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.DataTypeNotContentProperty, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Cannot_Get_Items_For_List_View_Property_On_Content_When_Non_Existing_Data_Type() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + Guid.NewGuid(), + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.DataTypeNotFound, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Cannot_Get_Items_For_List_View_Property_On_Content_When_Data_Type_In_Not_List_View() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAndListViewProperty(); + + IDataType? textstringDataType = await DataTypeService.GetAsync(Constants.DataTypes.Guids.TextstringGuid); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + textstringDataType.Key, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.DataTypeNotCollection, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task Cannot_Order_List_View_Items_By_Field_Not_Part_Of_Configured_List_View_Properties() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "testFieldNotPartOfListViewConfiguration", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.OrderByNotPartOfCollectionConfiguration, result.Status); + Assert.IsNull(result.Result); + }); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Order_List_View_Items_By_Name(bool orderAscending) // "name" is a special field - never part of configured list view properties + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + var descendants = ContentService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "name", + null, + orderAscending ? Direction.Ascending : Direction.Descending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel collectionItemsResult = result.Result.Items; + var expectedNames = descendants + .Select(c => c.Name) + .OrderByDescending(name => orderAscending ? null : name); + var actualNames = collectionItemsResult.Items.Select(c => c.Name); + + Assert.Multiple(() => + { + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + Assert.IsTrue(expectedNames.SequenceEqual(actualNames)); + }); + } + + [TestCase("en-US", true, "Child item 1", "Child item 5")] + [TestCase("en-US", false, "Child item 5", "Child item 1")] + [TestCase("da-DK", true, "Child item 5", "Child item 1")] + [TestCase("da-DK", false, "Child item 1", "Child item 5")] + public async Task Can_Order_List_View_Items_By_Culture(string culture, bool orderAscending, string expectedFirstItemName, string expectedLastItemName) + { + // Arrange + var root = await CreateVariantRootContentWithFiveChildrenAsListViewItems(Constants.DataTypes.Guids.ListViewContentGuid); + var descendants = ContentService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "name", + culture, + orderAscending ? Direction.Ascending : Direction.Descending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel collectionItemsResult = result.Result.Items; + + Assert.Multiple(() => + { + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + Assert.AreEqual(expectedFirstItemName, collectionItemsResult.Items.First().Name); + Assert.AreEqual(expectedLastItemName, collectionItemsResult.Items.Last().Name); + }); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Order_List_View_Items_By_System_List_View_Field(bool orderAscending) + { + // Arrange + const string orderByField = "sortOrder"; + var root = await CreateRootContentWithFiveChildrenAndListViewProperty(); + var descendants = ContentService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + CustomListViewKey, + orderByField, + null, + orderAscending ? Direction.Ascending : Direction.Descending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel collectionItemsResult = result.Result.Items; + ListViewConfiguration collectionConfiguration = result.Result.ListViewConfiguration; + var expectedNames = descendants + .OrderByDescending(c => orderAscending ? default : c.SortOrder) + .Select(c => c.Name); + var actualNames = collectionItemsResult.Items.Select(c => c.Name); + + var sortOrderProperty = collectionConfiguration.IncludeProperties.FirstOrDefault(p => p.Alias == "sortOrder"); + + Assert.Multiple(() => + { + Assert.IsTrue(sortOrderProperty.IsSystem); + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + Assert.IsTrue(expectedNames.SequenceEqual(actualNames)); + }); + } + + [Test] + public async Task Can_Order_List_View_Items_By_Custom_List_View_Field() + { + // Arrange + const string orderByField = "price"; + var root = await CreateRootContentWithFiveChildrenAndListViewProperty(); + var descendants = ContentService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + CustomListViewKey, + orderByField, + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel collectionItemsResult = result.Result.Items; + ListViewConfiguration collectionConfiguration = result.Result.ListViewConfiguration; + var expectedNames = descendants.Select(c => c.Name); + var actualNames = collectionItemsResult.Items.Select(c => c.Name); + + var priceProperty = collectionConfiguration.IncludeProperties.FirstOrDefault(p => p.Alias == "price"); + + Assert.Multiple(() => + { + Assert.IsFalse(priceProperty.IsSystem); + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + Assert.IsTrue(expectedNames.SequenceEqual(actualNames)); + }); + } + + [TestCase("00000003-0000-0000-0000-000000000000", "Item 3")] + [TestCase("Item 3", "Item 3")] + public async Task Can_Filter_List_View_Items(string filter, string expectedName) + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + + var allChildren = ContentService.GetPagedChildren(root.Id, 0, 10, out _).ToArray(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + filter, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + + PagedModel filteredCollectionItems = result.Result.Items; + var actualFirst = filteredCollectionItems.Items.First(); + + Assert.Multiple(() => + { + Assert.AreEqual(5, allChildren.Length); + Assert.AreEqual(1, filteredCollectionItems.Total); + Assert.AreEqual(expectedName, actualFirst.Name); + }); + } + + [Test] + public async Task Can_Filter_List_View_Items_By_Random_Key_And_Get_Zero_Matches() + { + // Arrange + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + Guid.NewGuid().ToString(), + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + Assert.AreEqual(0, result.Result.Items.Total); + } + + [Test] + public async Task Cannot_Get_List_View_Items_That_The_User_Does_Not_Have_Access_To() + { + // Arrange + var contentType = ContentTypeBuilder.CreateSimpleContentType(); + contentType.AllowedAsRoot = true; + ContentTypeService.Save(contentType); + + var createModel = new ContentCreateModel + { + ContentTypeKey = contentType.Key, + InvariantName = "Page", + }; + + // Content that serves as a start node + var contentCreateResult = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + // New user and user group + var userGroup = new UserGroupBuilder() + .WithAlias("test") + .WithName("Test") + .WithAllowedSections(new[] { "packages" }) + .WithStartContentId(contentCreateResult.Result.Content.Id) + .Build(); + var userGroupCreateResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + var userCreateModel = new UserCreateModel + { + UserName = "testUser@mail.com", + Email = "testUser@mail.com", + Name = "Test user", + UserGroupKeys = new HashSet { userGroupCreateResult.Result.Key }, + }; + + var userCreateResult = await UserService.CreateAsync(Constants.Security.SuperUserKey, userCreateModel, true); + + // Content with items that the user doesn't have access to + var root = await CreateRootContentWithFiveChildrenAsListViewItems(); + + // Act + var result = await ContentListViewService.GetListViewItemsByKeyAsync( + userCreateResult.Result.CreatedUser, + root.Key, + null, + "updateDate", + null, + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + }); + Assert.AreEqual(0, result.Result.Items.Items.Count()); + } + + private async Task CreateCustomListViewDataType(IDictionary listViewConfiguration) + { + // Overwrite default IncludeProperties added by ListViewConfiguration + var configurationEditor = new ListViewConfigurationEditor(IOHelper) { DefaultConfiguration = listViewConfiguration }; + + var dataType = new DataTypeBuilder() + .WithId(0) + .WithKey(CustomListViewKey) + .WithName("Custom list view") + .WithDatabaseType(ValueStorageType.Nvarchar) + .AddEditor() + .WithAlias(Constants.PropertyEditors.Aliases.ListView) + .WithConfigurationEditor(configurationEditor) + .Done() + .Build(); + + var result = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.IsTrue(result.Result.ConfigurationObject is ListViewConfiguration); + }); + + return result.Result; + } + + private async Task CreateRootContentWithFiveChildrenAndListViewProperty() + { + var listViewConfiguration = new Dictionary + { + ["includeProperties"] = new[] + { + new Dictionary { { "alias", "sortOrder" }, { "isSystem", true } }, + new Dictionary { { "alias", "updateDate" }, { "isSystem", true } }, + new Dictionary { { "alias", "owner" }, { "isSystem", true } }, + new Dictionary { { "alias", "itemName" }, { "isSystem", false } }, + new Dictionary { { "alias", "price" }, { "isSystem", false } }, + }, + }; + + var customListView = await CreateCustomListViewDataType(listViewConfiguration); + + var childContentType = new ContentTypeBuilder() + .WithAlias("product") + .WithName("Product") + .AddPropertyType() + .WithAlias("itemName") + .WithName("Item Name") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithAlias("price") + .WithName("Price") + .WithDataTypeId(-51) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Integer) + .WithValueStorageType(ValueStorageType.Integer) + .WithSortOrder(2) + .Done() + .Build(); + + ContentTypeService.Save(childContentType); + + var contentTypeWithListViewPropertyType = new ContentTypeBuilder() + .WithAlias("products") + .WithName("Products") + .WithContentVariation(ContentVariation.Nothing) + .AddPropertyType() + .WithAlias("items") + .WithName("Items") + .WithDataTypeId(customListView.Id) + .WithPropertyEditorAlias(customListView.EditorAlias) + .Done() + .Build(); + + contentTypeWithListViewPropertyType.AllowedAsRoot = true; + contentTypeWithListViewPropertyType.AllowedContentTypes = new[] + { + new ContentTypeSort(childContentType.Key, 1, childContentType.Alias), + }; + ContentTypeService.Save(contentTypeWithListViewPropertyType); + + var rootContentCreateModel = new ContentCreateModel + { + ContentTypeKey = contentTypeWithListViewPropertyType.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Products", + }; + + var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); + var root = result.Result.Content; + + for (var i = 1; i < 6; i++) + { + var createModel = new ContentCreateModel + { + ContentTypeKey = childContentType.Key, + ParentKey = root.Key, + InvariantName = $"Item {i}", + Key = i.ToGuid(), + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "itemName", Value = $"Item {i}" }, + new PropertyValueModel { Alias = "price", Value = i * 10 }, + }, + }; + + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + return root; + } + + private async Task CreateRootContentWithFiveChildrenAsListViewItems(Guid? listViewDataTypeKey = null) + { + var childContentType = new ContentTypeBuilder() + .WithAlias("product") + .WithName("Product") + .Build(); + ContentTypeService.Save(childContentType); + + var contentTypeWithListView = new ContentTypeBuilder() + .WithAlias("products") + .WithName("Products") + .WithContentVariation(ContentVariation.Nothing) + .WithIsContainer(listViewDataTypeKey ?? Constants.DataTypes.Guids.ListViewContentGuid) + .Build(); + + contentTypeWithListView.AllowedAsRoot = true; + contentTypeWithListView.AllowedContentTypes = new[] + { + new ContentTypeSort(childContentType.Key, 1, childContentType.Alias), + }; + ContentTypeService.Save(contentTypeWithListView); + + var rootContentCreateModel = new ContentCreateModel + { + ContentTypeKey = contentTypeWithListView.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Products", + }; + + var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); + var root = result.Result.Content; + + for (var i = 1; i < 6; i++) + { + var createModel = new ContentCreateModel + { + ContentTypeKey = childContentType.Key, + ParentKey = root.Key, + InvariantName = $"Item {i}", + Key = i.ToGuid(), + }; + + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + return root; + } + + private async Task CreateVariantRootContentWithFiveChildrenAsListViewItems(Guid? listViewDataTypeKey = null) + { + var language = new LanguageBuilder() + .WithCultureInfo("da-DK") + .Build(); + await LanguageService.CreateAsync(language, Constants.Security.SuperUserKey); + + var childContentType = new ContentTypeBuilder() + .WithAlias("product") + .WithName("Product") + .WithContentVariation(ContentVariation.Culture) + .Build(); + ContentTypeService.Save(childContentType); + + var contentTypeWithListView = new ContentTypeBuilder() + .WithAlias("products") + .WithName("Products") + .WithContentVariation(ContentVariation.Culture) + .WithIsContainer(listViewDataTypeKey ?? Constants.DataTypes.Guids.ListViewContentGuid) + .Build(); + + contentTypeWithListView.AllowedAsRoot = true; + contentTypeWithListView.AllowedContentTypes = new[] + { + new ContentTypeSort(childContentType.Key, 1, childContentType.Alias), + }; + ContentTypeService.Save(contentTypeWithListView); + + var rootContentCreateModel = new ContentCreateModel + { + ContentTypeKey = contentTypeWithListView.Key, + Variants = new[] + { + new VariantModel + { + Culture = "en-US", + Name = "English Page", + Properties = Enumerable.Empty(), + }, + new VariantModel + { + Culture = "da-DK", + Name = "Danish Page", + Properties = Enumerable.Empty(), + }, + }, + }; + + var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); + var root = result.Result.Content; + + for (var i = 1; i < 6; i++) + { + var createModel = new ContentCreateModel + { + ContentTypeKey = childContentType.Key, + ParentKey = root.Key, + Variants = new[] + { + new VariantModel + { + Culture = "en-US", + Name = $"Child item {i}", + Properties = Enumerable.Empty(), + }, + new VariantModel + { + Culture = "da-DK", + Name = $"(DA) Child item {5 - i}", + Properties = Enumerable.Empty(), + }, + }, + }; + + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + return root; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTestsBase.cs new file mode 100644 index 0000000000..b0a61fa7a7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentListViewServiceTestsBase.cs @@ -0,0 +1,53 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public abstract class ContentListViewServiceTestsBase : UmbracoIntegrationTest +{ + protected IDataTypeService DataTypeService => GetRequiredService(); + + protected IUserGroupService UserGroupService => GetRequiredService(); + + protected IUserService UserService => GetRequiredService(); + + protected async Task GetSuperUser() + => await UserService.GetAsync(Constants.Security.SuperUserKey); + + protected async Task AssertListViewConfiguration(ListViewConfiguration actualConfiguration, Guid expectedListViewDataTypeKey) + { + var actualCollectionPropertyAliases = actualConfiguration + .IncludeProperties + .Select(p => p.Alias) + .WhereNotNull() + .ToArray(); + + // The configured list view + var expectedContentListViewConfig = await GetListViewConfigurationFromListViewDataType(expectedListViewDataTypeKey); + var expectedCollectionPropertyAliases = expectedContentListViewConfig + .IncludeProperties + .Select(p => p.Alias) + .WhereNotNull() + .ToArray(); + + Assert.AreEqual(expectedCollectionPropertyAliases.Length, actualCollectionPropertyAliases.Length); + Assert.IsTrue(expectedCollectionPropertyAliases.SequenceEqual(actualCollectionPropertyAliases)); + } + + private async Task GetListViewConfigurationFromListViewDataType(Guid dataTypeKey) + { + IDataType? dataType = await DataTypeService.GetAsync(dataTypeKey); + var listViewConfiguration = dataType.ConfigurationObject; + Assert.IsTrue(listViewConfiguration is ListViewConfiguration); + + return listViewConfiguration as ListViewConfiguration; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaListViewServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaListViewServiceTests.cs new file mode 100644 index 0000000000..d5a66439a7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaListViewServiceTests.cs @@ -0,0 +1,244 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +/// +/// Tests for the media list view service. Please notice that a lot of functional test is covered by the content list +/// view service tests, since these services share the same base implementation. +/// +public class MediaListViewServiceTests : ContentListViewServiceTestsBase +{ + private IMediaListViewService MediaListViewService => GetRequiredService(); + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private IMediaService MediaService => GetRequiredService(); + + private IMediaEditingService MediaEditingService => GetRequiredService(); + + private IUser SuperUser { get; set; } + + [SetUp] + public async Task Setup() + => SuperUser = await GetSuperUser(); + + [Test] + public async Task Can_Get_List_View_Items_At_Root() + { + // Arrange + CreateTenMediaItemsFromTwoMediaTypesAtRoot(); + var descendants = MediaService.GetPagedDescendants(Constants.System.Root, 0, int.MaxValue, out _); + + // Act + var result = await MediaListViewService.GetListViewItemsByKeyAsync( + SuperUser, + null, + null, + "updateDate", + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + + PagedModel collectionItemsResult = result.Result.Items; + + Assert.AreEqual(10, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + }); + } + + [Test] + public async Task Can_Get_Items_With_Default_List_View_Configuration() + { + // Arrange + CreateTenMediaItemsFromTwoMediaTypesAtRoot(); + + // Act + var result = await MediaListViewService.GetListViewItemsByKeyAsync( + SuperUser, + null, + null, + "updateDate", + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(async () => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + + await AssertListViewConfiguration(result.Result.ListViewConfiguration, Constants.DataTypes.Guids.ListViewMediaGuid); + }); + } + + [Test] + public async Task Can_Get_List_View_Items_By_Key() + { + // Arrange + var root = await CreateRootMediaWithFiveChildrenAsListViewItems(); + var descendants = MediaService.GetPagedDescendants(root.Id, 0, int.MaxValue, out _); + + // Act + var result = await MediaListViewService.GetListViewItemsByKeyAsync( + SuperUser, + root.Key, + null, + "updateDate", + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + // Assert the content type is configured as list view + Assert.IsNotNull(root.ContentType.ListView); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + + PagedModel collectionItemsResult = result.Result.Items; + + Assert.AreEqual(5, collectionItemsResult.Total); + CollectionAssert.AreEquivalent(descendants, collectionItemsResult.Items); + }); + } + + [Test] + public async Task Can_Only_Get_List_View_Items_That_The_User_Has_Access_To() + { + // Arrange + // Media item that the user doesn't have access to + var imageMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Image); + var image = MediaBuilder.CreateMediaImage(imageMediaType, -1); + MediaService.Save(image); + + // Media item that serves as a start node + var album = await CreateRootMediaWithFiveChildrenAsListViewItems(); + + MediaService.GetPagedChildren(Constants.System.Root, 0, int.MaxValue, out var totalChildren); + + // New user and user group + var userGroup = new UserGroupBuilder() + .WithAlias("test") + .WithName("Test") + .WithAllowedSections(new[] { "packages" }) + .WithStartMediaId(album.Id) + .Build(); + var userGroupCreateResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + var userCreateModel = new UserCreateModel + { + UserName = "testUser@mail.com", + Email = "testUser@mail.com", + Name = "Test user", + UserGroupKeys = new HashSet { userGroupCreateResult.Result.Key }, + }; + + var userCreateResult = + await UserService.CreateAsync(Constants.Security.SuperUserKey, userCreateModel, true); + + // Act + var result = await MediaListViewService.GetListViewItemsByKeyAsync( + userCreateResult.Result.CreatedUser, + null, + null, + "updateDate", + Direction.Ascending, + null, + 0, + 10); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentCollectionOperationStatus.Success, result.Status); + Assert.IsNotNull(result.Result); + Assert.AreEqual(2, totalChildren); + + Assert.AreEqual(1, result.Result.Items.Items.Count()); + }); + + } + + private void CreateTenMediaItemsFromTwoMediaTypesAtRoot() + { + var mediaType1 = MediaTypeBuilder.CreateImageMediaType("Image2"); + MediaTypeService.Save(mediaType1); + var mediaType2 = MediaTypeBuilder.CreateImageMediaType("Image3"); + MediaTypeService.Save(mediaType2); + + for (var i = 0; i < 5; i++) + { + var m1 = MediaBuilder.CreateMediaImage(mediaType1, -1); + MediaService.Save(m1); + var m2 = MediaBuilder.CreateMediaImage(mediaType2, -1); + MediaService.Save(m2); + } + } + + private async Task CreateRootMediaWithFiveChildrenAsListViewItems(Guid? listViewDataTypeKey = null) + { + var childImageMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Image); + + var mediaTypeWithListView = new MediaTypeBuilder() + .WithAlias("album") + .WithName("Album") + .WithIsContainer(listViewDataTypeKey ?? Constants.DataTypes.Guids.ListViewMediaGuid) + .Build(); + + mediaTypeWithListView.AllowedAsRoot = true; + mediaTypeWithListView.AllowedContentTypes = new[] + { + new ContentTypeSort(childImageMediaType.Key, 1, childImageMediaType.Alias), + }; + MediaTypeService.Save(mediaTypeWithListView); + + var rootContentCreateModel = new MediaCreateModel + { + ContentTypeKey = mediaTypeWithListView.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Album", + }; + + var result = await MediaEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); + var root = result.Result.Content; + + for (var i = 1; i < 6; i++) + { + var createModel = new MediaCreateModel + { + ContentTypeKey = childImageMediaType.Key, + ParentKey = root.Key, + InvariantName = $"Image {i}", + Key = i.ToGuid(), + }; + + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + return root; + } +}