From 223f091747803cf8a4aa13b0d49dae5bcd840f01 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Sun, 21 Jul 2024 22:10:28 -0400 Subject: [PATCH] Implemented content search. (#32) --- .../Contents/ContentSort.cs | 7 ++ .../Contents/ContentSortOption.cs | 20 ++++ .../Contents/SearchContentsPayload.cs | 11 ++ .../Queries/SearchContentTypesQueryHandler.cs | 6 +- .../Contents/IContentQuerier.cs | 3 + .../Contents/IContentRepository.cs | 1 + .../Contents/Queries/SearchContentsQuery.cs | 7 ++ .../Queries/SearchContentsQueryHandler.cs | 20 ++++ .../Entities/ContentItemEntity.cs | 10 +- .../Entities/ContentLocaleEntity.cs | 17 ++- .../Logitar.Cms.EntityFrameworkCore/Mapper.cs | 13 +++ .../Queriers/ContentQuerier.cs | 75 ++++++++++++- .../Queriers/ContentTypeQuerier.cs | 4 +- .../Repositories/ContentRepository.cs | 5 + .../Controllers/ContentController.cs | 9 ++ .../Contents/SearchContentsParameters.cs | 41 +++++++ .../SearchContentTypesQueryHandlerTests.cs | 20 ++-- .../SearchContentTypesQueryHandlerTests.cs | 100 ++++++++++++++++++ 18 files changed, 349 insertions(+), 20 deletions(-) create mode 100644 backend/src/Logitar.Cms.Contracts/Contents/ContentSort.cs create mode 100644 backend/src/Logitar.Cms.Contracts/Contents/ContentSortOption.cs create mode 100644 backend/src/Logitar.Cms.Contracts/Contents/SearchContentsPayload.cs create mode 100644 backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQuery.cs create mode 100644 backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQueryHandler.cs create mode 100644 backend/src/Logitar.Cms.Web/Models/Contents/SearchContentsParameters.cs create mode 100644 backend/tests/Logitar.Cms.Core.IntegrationTests/Contents/Queries/SearchContentTypesQueryHandlerTests.cs diff --git a/backend/src/Logitar.Cms.Contracts/Contents/ContentSort.cs b/backend/src/Logitar.Cms.Contracts/Contents/ContentSort.cs new file mode 100644 index 0000000..3763e7a --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/Contents/ContentSort.cs @@ -0,0 +1,7 @@ +namespace Logitar.Cms.Contracts.Contents; + +public enum ContentSort +{ + UniqueName, + UpdatedOn +} diff --git a/backend/src/Logitar.Cms.Contracts/Contents/ContentSortOption.cs b/backend/src/Logitar.Cms.Contracts/Contents/ContentSortOption.cs new file mode 100644 index 0000000..851cbc7 --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/Contents/ContentSortOption.cs @@ -0,0 +1,20 @@ +using Logitar.Cms.Contracts.Search; + +namespace Logitar.Cms.Contracts.Contents; + +public record ContentSortOption : SortOption +{ + public new ContentSort Field + { + get => Enum.Parse(base.Field); + set => base.Field = value.ToString(); + } + + public ContentSortOption() : this(ContentSort.UpdatedOn, isDescending: true) + { + } + + public ContentSortOption(ContentSort field, bool isDescending = false) : base(field.ToString(), isDescending) + { + } +} diff --git a/backend/src/Logitar.Cms.Contracts/Contents/SearchContentsPayload.cs b/backend/src/Logitar.Cms.Contracts/Contents/SearchContentsPayload.cs new file mode 100644 index 0000000..81695e5 --- /dev/null +++ b/backend/src/Logitar.Cms.Contracts/Contents/SearchContentsPayload.cs @@ -0,0 +1,11 @@ +using Logitar.Cms.Contracts.Search; + +namespace Logitar.Cms.Contracts.Contents; + +public record SearchContentsPayload : SearchPayload +{ + public Guid? ContentTypeId { get; set; } + public Guid? LanguageId { get; set; } + + public new List? Sort { get; set; } +} diff --git a/backend/src/Logitar.Cms.Core/ContentTypes/Queries/SearchContentTypesQueryHandler.cs b/backend/src/Logitar.Cms.Core/ContentTypes/Queries/SearchContentTypesQueryHandler.cs index 08c570e..5fb2229 100644 --- a/backend/src/Logitar.Cms.Core/ContentTypes/Queries/SearchContentTypesQueryHandler.cs +++ b/backend/src/Logitar.Cms.Core/ContentTypes/Queries/SearchContentTypesQueryHandler.cs @@ -6,15 +6,15 @@ namespace Logitar.Cms.Core.ContentTypes.Queries; internal class SearchContentTypesQueryHandler : IRequestHandler> { - private readonly IContentTypeQuerier _fieldTypeQuerier; + private readonly IContentTypeQuerier _contentTypeQuerier; public SearchContentTypesQueryHandler(IContentTypeQuerier fieldTypeQuerier) { - _fieldTypeQuerier = fieldTypeQuerier; + _contentTypeQuerier = fieldTypeQuerier; } public async Task> Handle(SearchContentTypesQuery query, CancellationToken cancellationToken) { - return await _fieldTypeQuerier.SearchAsync(query.Payload, cancellationToken); + return await _contentTypeQuerier.SearchAsync(query.Payload, cancellationToken); } } diff --git a/backend/src/Logitar.Cms.Core/Contents/IContentQuerier.cs b/backend/src/Logitar.Cms.Core/Contents/IContentQuerier.cs index 47dc020..2db2d65 100644 --- a/backend/src/Logitar.Cms.Core/Contents/IContentQuerier.cs +++ b/backend/src/Logitar.Cms.Core/Contents/IContentQuerier.cs @@ -1,4 +1,5 @@ using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; namespace Logitar.Cms.Core.Contents; @@ -7,4 +8,6 @@ public interface IContentQuerier Task ReadAsync(ContentAggregate content, CancellationToken cancellationToken = default); Task ReadAsync(ContentId id, CancellationToken cancellationToken = default); Task ReadAsync(Guid id, CancellationToken cancellationToken = default); + + Task> SearchAsync(SearchContentsPayload payload, CancellationToken cancellationToken = default); } diff --git a/backend/src/Logitar.Cms.Core/Contents/IContentRepository.cs b/backend/src/Logitar.Cms.Core/Contents/IContentRepository.cs index 4c10ec0..409f30e 100644 --- a/backend/src/Logitar.Cms.Core/Contents/IContentRepository.cs +++ b/backend/src/Logitar.Cms.Core/Contents/IContentRepository.cs @@ -6,6 +6,7 @@ namespace Logitar.Cms.Core.Contents; public interface IContentRepository { + Task> LoadAsync(CancellationToken cancellationToken = default); Task LoadAsync(ContentId id, CancellationToken cancellationToken = default); Task LoadAsync(ContentTypeId contentTypeId, LanguageId? languageId, UniqueNameUnit uniqueName, CancellationToken cancellationToken = default); diff --git a/backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQuery.cs b/backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQuery.cs new file mode 100644 index 0000000..625ac75 --- /dev/null +++ b/backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQuery.cs @@ -0,0 +1,7 @@ +using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; +using MediatR; + +namespace Logitar.Cms.Core.Contents.Queries; + +public record SearchContentsQuery(SearchContentsPayload Payload) : IRequest>; diff --git a/backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQueryHandler.cs b/backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQueryHandler.cs new file mode 100644 index 0000000..4789c0b --- /dev/null +++ b/backend/src/Logitar.Cms.Core/Contents/Queries/SearchContentsQueryHandler.cs @@ -0,0 +1,20 @@ +using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; +using MediatR; + +namespace Logitar.Cms.Core.Contents.Queries; + +internal class SearchContentsQueryHandler : IRequestHandler> +{ + private readonly IContentQuerier _contentQuerier; + + public SearchContentsQueryHandler(IContentQuerier fieldTypeQuerier) + { + _contentQuerier = fieldTypeQuerier; + } + + public async Task> Handle(SearchContentsQuery query, CancellationToken cancellationToken) + { + return await _contentQuerier.SearchAsync(query.Payload, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentItemEntity.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentItemEntity.cs index fb38f86..b81f2e5 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentItemEntity.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentItemEntity.cs @@ -28,7 +28,8 @@ private ContentItemEntity() : base() { } - public override IEnumerable GetActorIds() + public override IEnumerable GetActorIds() => GetActorIds(includeLocales: true); + public IEnumerable GetActorIds(bool includeLocales) { List actorIds = base.GetActorIds().ToList(); @@ -37,9 +38,12 @@ public override IEnumerable GetActorIds() actorIds.AddRange(ContentType.GetActorIds()); } - foreach (ContentLocaleEntity locale in Locales) + if (includeLocales) { - actorIds.AddRange(locale.GetActorIds()); + foreach (ContentLocaleEntity locale in Locales) + { + actorIds.AddRange(locale.GetActorIds(includeItem: false)); + } } return actorIds.AsReadOnly(); diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentLocaleEntity.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentLocaleEntity.cs index 9897044..6a85d49 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentLocaleEntity.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Entities/ContentLocaleEntity.cs @@ -76,7 +76,22 @@ private ContentLocaleEntity() { } - public IEnumerable GetActorIds() => [new(CreatedBy), new(UpdatedBy)]; + public IEnumerable GetActorIds(bool includeItem) + { + List actorIds = [new(CreatedBy), new(UpdatedBy)]; + + if (Language != null) + { + actorIds.AddRange(Language.GetActorIds()); + } + + if (includeItem && Item != null) + { + actorIds.AddRange(Item.GetActorIds(includeLocales: false)); + } + + return actorIds.AsReadOnly(); + } public void Update(ContentLocaleUnit locale, DomainEvent @event) { diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Mapper.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Mapper.cs index 5240b50..1a825f9 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Mapper.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Mapper.cs @@ -112,6 +112,19 @@ public ContentItem ToContentItem(ContentItemEntity source) return destination; } + + public ContentLocale ToContentLocale(ContentLocaleEntity source) + { + if (source.Item == null) + { + throw new ArgumentException($"The {nameof(source.Item)} is required.", nameof(source)); + } + + ContentItem item = ToContentItem(source.Item); + + return item.Locales.SingleOrDefault() ?? item.Invariant; + } + private ContentLocale ToContentLocale(ContentLocaleEntity source, ContentItem item) { if (source.LanguageId != null && source.Language == null) diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentQuerier.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentQuerier.cs index 5eb886e..e617c86 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentQuerier.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentQuerier.cs @@ -1,9 +1,12 @@ using Logitar.Cms.Contracts.Actors; using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; using Logitar.Cms.Core.Contents; using Logitar.Cms.EntityFrameworkCore.Actors; using Logitar.Cms.EntityFrameworkCore.Entities; +using Logitar.Data; using Logitar.EventSourcing; +using Logitar.Identity.EntityFrameworkCore.Relational; using Microsoft.EntityFrameworkCore; namespace Logitar.Cms.EntityFrameworkCore.Queriers; @@ -12,11 +15,17 @@ internal class ContentQuerier : IContentQuerier { private readonly IActorService _actorService; private readonly DbSet _contentItems; + private readonly DbSet _contentLocales; + private readonly ISearchHelper _searchHelper; + private readonly ISqlHelper _sqlHelper; - public ContentQuerier(IActorService actorService, CmsContext context) + public ContentQuerier(IActorService actorService, CmsContext context, ISearchHelper searchHelper, ISqlHelper sqlHelper) { _actorService = actorService; _contentItems = context.ContentItems; + _contentLocales = context.ContentLocales; + _searchHelper = searchHelper; + _sqlHelper = sqlHelper; } public async Task ReadAsync(ContentAggregate content, CancellationToken cancellationToken) @@ -38,6 +47,59 @@ public async Task ReadAsync(ContentAggregate content, CancellationT return content == null ? null : await MapAsync(content, cancellationToken); } + public async Task> SearchAsync(SearchContentsPayload payload, CancellationToken cancellationToken) + { + IQueryBuilder builder = _sqlHelper.QueryFrom(CmsDb.ContentLocales.Table).SelectAll(CmsDb.ContentLocales.Table) + .Join(CmsDb.ContentItems.ContentItemId, CmsDb.ContentLocales.ContentItemId) + .Join(CmsDb.ContentTypes.ContentTypeId, CmsDb.ContentItems.ContentTypeId) + .LeftJoin(CmsDb.Languages.LanguageId, CmsDb.ContentLocales.LanguageId) + .ApplyIdInFilter(CmsDb.ContentItems.UniqueId, payload); + _searchHelper.ApplyTextSearch(builder, payload.Search, CmsDb.ContentLocales.UniqueName); + + if (payload.ContentTypeId.HasValue) + { + builder.Where(CmsDb.ContentTypes.UniqueId, Operators.IsEqualTo(payload.ContentTypeId.Value.ToString())); + } + + ConditionalOperator @operator = payload.LanguageId.HasValue ? Operators.IsEqualTo(payload.LanguageId.Value.ToString()) : Operators.IsNull(); + builder.Where(CmsDb.Languages.UniqueId, @operator); + + IQueryable query = _contentLocales.FromQuery(builder).AsNoTracking() + .Include(x => x.Item).ThenInclude(x => x!.ContentType) + .Include(x => x.Language); + + long total = await query.LongCountAsync(cancellationToken); + + IOrderedQueryable? ordered = null; + if (payload.Sort != null) + { + foreach (ContentSortOption sort in payload.Sort) + { + switch (sort.Field) + { + case ContentSort.UniqueName: + ordered = (ordered == null) + ? (sort.IsDescending ? query.OrderByDescending(x => x.UniqueName) : query.OrderBy(x => x.UniqueName)) + : (sort.IsDescending ? ordered.ThenByDescending(x => x.UniqueName) : ordered.ThenBy(x => x.UniqueName)); + break; + case ContentSort.UpdatedOn: + ordered = (ordered == null) + ? (sort.IsDescending ? query.OrderByDescending(x => x.UpdatedOn) : query.OrderBy(x => x.UpdatedOn)) + : (sort.IsDescending ? ordered.ThenByDescending(x => x.UpdatedOn) : ordered.ThenBy(x => x.UpdatedOn)); + break; + } + } + } + query = ordered ?? query; + + query = query.ApplyPaging(payload); + + ContentLocaleEntity[] locales = await query.ToArrayAsync(cancellationToken); + IEnumerable items = await MapAsync(locales, cancellationToken); + + return new SearchResults(items, total); + } + private async Task MapAsync(ContentItemEntity content, CancellationToken cancellationToken) => (await MapAsync([content], cancellationToken)).Single(); private async Task> MapAsync(IEnumerable contents, CancellationToken cancellationToken) @@ -48,4 +110,15 @@ private async Task> MapAsync(IEnumerable MapAsync(ContentLocaleEntity locale, CancellationToken cancellationToken) + => (await MapAsync([locale], cancellationToken)).Single(); + private async Task> MapAsync(IEnumerable locales, CancellationToken cancellationToken) + { + IEnumerable actorIds = locales.SelectMany(locales => locales.GetActorIds(includeItem: true)); + IReadOnlyCollection actors = await _actorService.FindAsync(actorIds, cancellationToken); + Mapper mapper = new(actors); + + return locales.Select(mapper.ToContentLocale).ToArray(); + } } diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentTypeQuerier.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentTypeQuerier.cs index fcb0cec..a24cf95 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentTypeQuerier.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Queriers/ContentTypeQuerier.cs @@ -99,8 +99,8 @@ public async Task> SearchAsync(SearchContentTypesP query = query.ApplyPaging(payload); - ContentTypeEntity[] fieldTypes = await query.ToArrayAsync(cancellationToken); - IEnumerable items = await MapAsync(fieldTypes, cancellationToken); + ContentTypeEntity[] contentTypes = await query.ToArrayAsync(cancellationToken); + IEnumerable items = await MapAsync(contentTypes, cancellationToken); return new SearchResults(items, total); } diff --git a/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/ContentRepository.cs b/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/ContentRepository.cs index 7914a4b..f8af3d3 100644 --- a/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/ContentRepository.cs +++ b/backend/src/Logitar.Cms.EntityFrameworkCore/Repositories/ContentRepository.cs @@ -22,6 +22,11 @@ public ContentRepository(IEventBus eventBus, EventContext eventContext, IEventSe _sqlHelper = sqlHelper; } + public async Task> LoadAsync(CancellationToken cancellationToken) + { + return (await base.LoadAsync(cancellationToken)).ToArray(); + } + public async Task LoadAsync(ContentId id, CancellationToken cancellationToken) { return await base.LoadAsync(id.AggregateId, cancellationToken); diff --git a/backend/src/Logitar.Cms.Web/Controllers/ContentController.cs b/backend/src/Logitar.Cms.Web/Controllers/ContentController.cs index 8516b22..851d991 100644 --- a/backend/src/Logitar.Cms.Web/Controllers/ContentController.cs +++ b/backend/src/Logitar.Cms.Web/Controllers/ContentController.cs @@ -1,7 +1,9 @@ using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; using Logitar.Cms.Core; using Logitar.Cms.Core.Contents.Commands; using Logitar.Cms.Core.Contents.Queries; +using Logitar.Cms.Web.Models.Contents; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -48,4 +50,11 @@ public async Task> SaveLocaleAsync(Guid contentId, Gui ContentItem? content = await _pipeline.ExecuteAsync(new SaveContentLocaleCommand(contentId, languageId, payload), cancellationToken); return content == null ? NotFound() : Ok(content); } + + [HttpGet] + public async Task>> SearchAsync([FromQuery] SearchContentsParameters parameters, CancellationToken cancellationToken) + { + SearchResults contents = await _pipeline.ExecuteAsync(new SearchContentsQuery(parameters.ToPayload()), cancellationToken); + return Ok(contents); + } } diff --git a/backend/src/Logitar.Cms.Web/Models/Contents/SearchContentsParameters.cs b/backend/src/Logitar.Cms.Web/Models/Contents/SearchContentsParameters.cs new file mode 100644 index 0000000..54db002 --- /dev/null +++ b/backend/src/Logitar.Cms.Web/Models/Contents/SearchContentsParameters.cs @@ -0,0 +1,41 @@ +using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; +using Logitar.Cms.Web.Models.Search; +using Microsoft.AspNetCore.Mvc; + +namespace Logitar.Cms.Web.Models.Contents; + +public record SearchContentsParameters : SearchParameters +{ + [FromQuery(Name = "type")] + public Guid? ContentTypeId { get; set; } + + [FromQuery(Name = "language")] + public Guid? LanguageId { get; set; } + + public SearchContentsPayload ToPayload() + { + SearchContentsPayload payload = new() + { + ContentTypeId = ContentTypeId, + LanguageId = LanguageId + }; + + FillPayload(payload); + + var sortOptions = ((SearchPayload)payload).Sort; + if (sortOptions != null) + { + payload.Sort = new List(capacity: sortOptions.Count); + foreach (var sort in sortOptions) + { + if (Enum.TryParse(sort.Field, out ContentSort field)) + { + payload.Sort.Add(new ContentSortOption(field, sort.IsDescending)); + } + } + } + + return payload; + } +} diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/ContentTypes/Queries/SearchContentTypesQueryHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/ContentTypes/Queries/SearchContentTypesQueryHandlerTests.cs index c53b210..81190f6 100644 --- a/backend/tests/Logitar.Cms.Core.IntegrationTests/ContentTypes/Queries/SearchContentTypesQueryHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/ContentTypes/Queries/SearchContentTypesQueryHandlerTests.cs @@ -7,7 +7,7 @@ namespace Logitar.Cms.Core.ContentTypes.Queries; [Trait(Traits.Category, Categories.Integration)] public class SearchContentTypesQueryHandlerTests : IntegrationTests { - private readonly IContentTypeRepository _fieldTypeRepository; + private readonly IContentTypeRepository _contentTypeRepository; private readonly ContentTypeAggregate _article; private readonly ContentTypeAggregate _blog; @@ -17,7 +17,7 @@ public class SearchContentTypesQueryHandlerTests : IntegrationTests public SearchContentTypesQueryHandlerTests() : base() { - _fieldTypeRepository = ServiceProvider.GetRequiredService(); + _contentTypeRepository = ServiceProvider.GetRequiredService(); _article = new(new IdentifierUnit("BlogArticle"), isInvariant: false); _blog = new(new IdentifierUnit("Blog"), isInvariant: false); @@ -30,11 +30,11 @@ public override async Task InitializeAsync() { await base.InitializeAsync(); - await _fieldTypeRepository.SaveAsync([_article, _blog, _author, _magazine, _product]); + await _contentTypeRepository.SaveAsync([_article, _blog, _author, _magazine, _product]); } - [Fact(DisplayName = "It should return empty results when no field type matches.")] - public async Task It_should_return_empty_results_when_no_field_type_matches() + [Fact(DisplayName = "It should return empty results when no content type matches.")] + public async Task It_should_return_empty_results_when_no_content_type_matches() { SearchContentTypesPayload payload = new() { @@ -48,13 +48,13 @@ public async Task It_should_return_empty_results_when_no_field_type_matches() Assert.Equal(0, results.Total); } - [Fact(DisplayName = "It should return the correct matching field types.")] - public async Task It_should_return_the_correct_matching_field_types() + [Fact(DisplayName = "It should return the correct matching content types.")] + public async Task It_should_return_the_correct_matching_content_types() { SearchContentTypesPayload payload = new() { IsInvariant = false, - IdIn = (await _fieldTypeRepository.LoadAsync()).Select(fieldType => fieldType.Id.ToGuid()).ToList(), + IdIn = (await _contentTypeRepository.LoadAsync()).Select(contentType => contentType.Id.ToGuid()).ToList(), Search = new TextSearch([new SearchTerm("blog%"), new SearchTerm("%z%")], SearchOperator.Or), Sort = [new ContentTypeSortOption(ContentTypeSort.UniqueName, isDescending: true)], Skip = 1, @@ -67,7 +67,7 @@ public async Task It_should_return_the_correct_matching_field_types() SearchResults results = await Pipeline.ExecuteAsync(query); Assert.Equal(2, results.Total); - CmsContentType fieldType = Assert.Single(results.Items); - Assert.Equal(_article.Id.ToGuid(), fieldType.Id); + CmsContentType contentType = Assert.Single(results.Items); + Assert.Equal(_article.Id.ToGuid(), contentType.Id); } } diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/Contents/Queries/SearchContentTypesQueryHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/Contents/Queries/SearchContentTypesQueryHandlerTests.cs new file mode 100644 index 0000000..417de05 --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/Contents/Queries/SearchContentTypesQueryHandlerTests.cs @@ -0,0 +1,100 @@ +using Logitar.Cms.Contracts.Contents; +using Logitar.Cms.Contracts.Search; +using Logitar.Cms.Core.ContentTypes; +using Logitar.Cms.Core.Languages; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Core.Contents.Queries; + +[Trait(Traits.Category, Categories.Integration)] +public class SearchContentsQueryHandlerTests : IntegrationTests +{ + private readonly IContentRepository _contentRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ILanguageRepository _languageRepository; + + private readonly ContentTypeAggregate _articleType; + private readonly ContentTypeAggregate _productType; + + private readonly ContentAggregate _integraTypeS; + private readonly ContentAggregate _renderedLegoModels; + private readonly ContentAggregate _electricVisionDesign; + private readonly ContentAggregate _integraFrontSplitter; + private readonly ContentAggregate _customGrillesTlx; + private readonly ContentAggregate _tlxTypeS; + + public SearchContentsQueryHandlerTests() : base() + { + _contentRepository = ServiceProvider.GetRequiredService(); + _contentTypeRepository = ServiceProvider.GetRequiredService(); + _languageRepository = ServiceProvider.GetRequiredService(); + + _articleType = new(new IdentifierUnit("BlogArticle"), isInvariant: false); + _productType = new(new IdentifierUnit("Product"), isInvariant: false); + + IUniqueNameSettings uniqueNameSettings = ContentAggregate.UniqueNameSettings; + _integraTypeS = new(_articleType, new ContentLocaleUnit(new UniqueNameUnit(uniqueNameSettings, "video-integra-type-s-makes-605-whp-on-stock-internals"))); + _renderedLegoModels = new(_articleType, new ContentLocaleUnit(new UniqueNameUnit(uniqueNameSettings, "rendered-lego-acura-models"))); + _electricVisionDesign = new(_articleType, new ContentLocaleUnit(new UniqueNameUnit(uniqueNameSettings, "acura-previews-performance-electric-vision-design-study-at-monterey-car-week"))); + _integraFrontSplitter = new(_articleType, new ContentLocaleUnit(new UniqueNameUnit(uniqueNameSettings, "gallery-2023-acura-integra-front-splitter-options"))); + _customGrillesTlx = new(_articleType, new ContentLocaleUnit(new UniqueNameUnit(uniqueNameSettings, "custom-grilles-for-the-2015-2017-acura-tlx"))); + _tlxTypeS = new(_productType, new ContentLocaleUnit(new UniqueNameUnit(uniqueNameSettings, "acura-tlx-type-s"))); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _contentTypeRepository.SaveAsync([_articleType, _productType]); + await _contentRepository.SaveAsync([_integraTypeS, _renderedLegoModels, _electricVisionDesign, _integraFrontSplitter, _customGrillesTlx, _tlxTypeS]); + } + + [Fact(DisplayName = "It should return empty results when no content matches.")] + public async Task It_should_return_empty_results_when_no_content_matches() + { + SearchContentsPayload payload = new() + { + Search = new TextSearch([new SearchTerm("%test%")]) + }; + SearchContentsQuery query = new(payload); + + SearchResults results = await Pipeline.ExecuteAsync(query); + + Assert.Empty(results.Items); + Assert.Equal(0, results.Total); + } + + [Fact(DisplayName = "It should return the correct matching contents.")] + public async Task It_should_return_the_correct_matching_contents() + { + LanguageAggregate english = await _languageRepository.LoadDefaultAsync(); + _integraTypeS.SetLocale(english, _integraTypeS.Invariant); + _renderedLegoModels.SetLocale(english, _renderedLegoModels.Invariant); + _electricVisionDesign.SetLocale(english, _electricVisionDesign.Invariant); + _customGrillesTlx.SetLocale(english, _customGrillesTlx.Invariant); + _tlxTypeS.SetLocale(english, _tlxTypeS.Invariant); + await _contentRepository.SaveAsync([_integraTypeS, _renderedLegoModels, _electricVisionDesign, _customGrillesTlx, _tlxTypeS]); + + SearchContentsPayload payload = new() + { + ContentTypeId = _articleType.Id.ToGuid(), + LanguageId = english.Id.ToGuid(), + IdIn = (await _contentRepository.LoadAsync()).Select(content => content.Id.ToGuid()).ToList(), + Search = new TextSearch([new SearchTerm("%acura%")]), + Sort = [new ContentSortOption(ContentSort.UniqueName, isDescending: false)], + Skip = 1, + Limit = 1 + }; + payload.IdIn.Add(Guid.Empty); + payload.IdIn.Remove(_electricVisionDesign.Id.ToGuid()); + SearchContentsQuery query = new(payload); + + SearchResults results = await Pipeline.ExecuteAsync(query); + + Assert.Equal(2, results.Total); + ContentLocale locale = Assert.Single(results.Items); + Assert.Equal(_renderedLegoModels.Id.ToGuid(), locale.Item.Id); + } +}