* Add the core parts of the headless PoC * Add Content API project (WIP - loads of TODOs and dupes that need to be fixed!) * Rename the content API project and namespaces * Fixed bad merge * Rename everything "Headless" to "ContentApi" or "Api" * Refactor Content + Media: Key => Id, Name not nullable * Make Content API property return value types independent of datatype configuration * Clean up refactorings * First stab at an expansion strategy using content picker as example implementation * Use named JSON options for content API serialization * Proper inclusion and registration of the content API * Introduce API media builder * Make MNTP return API content/media depending on configuration (instead of links) and support output expansion * Content API: Get by controllers (#13740) * Adding ContentApiControllerBase * Adding get by id and url controllers * Change route of get all test controller * Rename to ContentApiController * Refactoring * Removing test controller * Content API: Add start-node header value to deal with url collisions (#13746) * Use start-node header value to deal with url collisions * Cleanup * Rename "url" param to "path" * Adding a start node service to get the start-node header value * Trim '/' from both beginning and end * Content API: Support Accept-Language header (#13831) * Move the content API JSON type resolver to an appropriate namespace * Add localization based on Accept-Language header * Content API: Output expansion (#13848) * Implement request based output expansion strategy + expansion output cache at property level * Slighty leaner implementation for default output expansion strategy * Clarify the code a bit * Fix bad merge * Encapsulate content API dependencies in the DI * Support multi-site and multi-culture routing + a little rename/refactor (#13882) * Support multi-site and multi-culture routing + a little rename/refactor * Make the by route controller handle root nodes * Rename Url to Path in API content output * Add a few comments for magic route creation * Rename services from "Default" to "Noop" * Ensure that Umbraco can boot without adding "AddContentApi()" to ConfigureServices * Moved incorrectly placed media builder * Fix API routes (#13915) * Fix multi URL picker value converter trying to access disposed objects in edge cases * Delivery API: Content routing and structure (#13984) * Introduce content route instead of content path, rename and rework start item (previously start node) handling * Strip out start node path in generated route path * Make the start-item header take precedence over the request domain * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Include umbraco properties (width, height, ...) in the Media Properties collection (#14023) * Move umbraco properties (width, height, ...) to the Properties collection of the API Media model * Don't output the umbracoFile property of media items * Add content type deny list (#14025) * Create a deny list of content types and utilize it for output generation * Add unit tests * Dedicated property cache level for Content API (#14027) * Support redirect tracking (#14033) * Create a deny list of content types and utilize it for output generation * Add unit tests * Handle redirect tracking in the content API * Include start item routing info for redirects * Add cultures and their routes to the API output (#14038) * Create a deny list of content types and utilize it for output generation * Add unit tests * Handle redirect tracking in the content API * Include start item routing info for redirects * Add culture routes to root output (for HREFLANG support) * Rename redirect service method to better match its purpose * Review changes * Delivery API: Query controller (#14041) * Initial commit * Custom ContentAPIFieldDefinitionCollection * Make index IUmbracoContentIndex * Add querying for children by parent id (key) * Add missing interface * Adding querying endpoint * Test code * Compose unpublishedValueSet, so that you get the correct data in the ContentAPI index * Renaming * Fix ancestorKeys index values * Adding IApiQueryExtensionService to be able to query the ContentAPI index in a generic way * Fix IApiQueryService and clean up QueryContentApiController using it * Support querying for path * Fix content API indexing * Fix default sorting * Implement concrete QueryOption implementations * Introduce new ExecuteQuery that uses the Core OptionHandlers * Implement ExecuteQuery * Change ExecuteQuery signature and implementation * Implement demo sorting and fetching * Add query option handlers and collection builder for them * Cleanup * Revert "Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)" This reverts commit 78e1f748e55383baecd123d06457111e18f13365. * Revert "Delivery API: Content routing and structure (#13984)" This reverts commit a0292ae5350362dd6c1c5bc9763deda928c78a75. * Revert "Fix multi URL picker value converter trying to access disposed objects in edge cases" This reverts commit 6b7c37a5bf7871bee93a2b2640bbc6ef591f14db. * Revert "Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)" This reverts commit 78e1f748e55383baecd123d06457111e18f13365. * Revert "Delivery API: Content routing and structure (#13984)" This reverts commit a0292ae5350362dd6c1c5bc9763deda928c78a75. * Revert "Fix multi URL picker value converter trying to access disposed objects in edge cases" This reverts commit 6b7c37a5bf7871bee93a2b2640bbc6ef591f14db. * Fix multi URL picker value converter trying to access disposed objects in edge cases * Delivery API: Content routing and structure (#13984) * Introduce content route instead of content path, rename and rework start item (previously start node) handling * Strip out start node path in generated route path * Make the start-item header take precedence over the request domain * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Test commit * Refactored interfaces for the query handlers and for the selectors (that will handle the value of the fetch query option) * Implemented a base class for the query options * Refactored the names of the selectors and made use of the base class * Refactored the ApiQueryService * Refactored the QueryContentApiController.cs * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Fixing merge gone wrong * Fix multi URL picker value converter trying to access disposed objects in edge cases * Delivery API: Content routing and structure (#13984) * Introduce content route instead of content path, rename and rework start item (previously start node) handling * Strip out start node path in generated route path * Make the start-item header take precedence over the request domain * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Make fetching work with the new setup * Moving files to dedicated folders * Removing ? for array * Rename selector query method * Implement FilterHandler and some filters * Implement SortHandler and sort some sorts * Refactoring * Adding more fields to index due to querying * Appending filtering and sorting queries * Implementing a new ISelectorHandler without Examine types * Re-implementing the collection to have a dedicated one for the selectors * Implementing a new IFilterHandler without Examine types & refactoring the filters implementing it * Adding a new collection dedicated to filters * Renaming the old collection * Implementing a new ISortHandler without Examine types & refactoring the sorts implementing it * Adding a new collection for the sorts & adding all collections to UmbracoBuilder.Collections * Refactoring the service to use the new collections and types * Refactoring the fields in ContentApiFieldDefinitionCollection * Remove nullability in Handlers * Don't return null for selector * Add TODO for having the filters support negation * Changing the SortType to FieldType with our custom types on the SortOption * Fix AncestorsSelector * Fix ApiQueryService * Documentation * Fix Swagger docs * Refactor the QueryContentApiController * Adding handling for the IApiContentResponse in the JsonTypeResolver * Refactor the service to use a safe fallback value in Examine queries * Adding Noop for the IApiQueryService * Cleanup * Remove comment * Fix name field for indexing * Don't inherit QueryOptionBase in filters * Fix casing for API index constant + swap FIXME with TODO * Add TODO for handling missing fetch with start-item header * Rename query handler parameters to not leak source (i.e. query string) --------- Co-authored-by: kjac <kja@umbraco.dk> Co-authored-by: Elitsa <> Co-authored-by: Zeegaan <nge@umbraco.dk> * Delivery API: Adding pagination to query endpoint (#14083) * Adding pagination to query endpoint * Optimize the paging using Examine directly * Fix comment * Remove skip/take code duplication --------- Co-authored-by: kjac <kja@umbraco.dk> * Add missing CompatibilitySuppressions.xml * Make Delivery API packable * Make Api.Common packable * Renamed extension method and namespace so it is discoverable * Untangle ApiVersion configuration into api.common, so delivery api do not require the management api to boot. * configure options in management api * RTE output as JSON for Content API (#14067) * Conditionally serve RTE output as JSON instead of HTML * Fixed merge * Rename to Delivery API (#14119) * Rename ContentApi to DeliveryApi * Rename delivery API index implementation * Update comments from "Content API" to "Delivery API" * Rename project from Content to Delivery * Add dedicated controller base for content delivery API * Rename delivery API content index to include "content" specifically * Fix compat suppressions --------- Co-authored-by: kjac <kja@umbraco.dk> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Zeegaan <nge@umbraco.dk>
201 lines
8.8 KiB
C#
201 lines
8.8 KiB
C#
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
using Umbraco.Cms.Core.DeliveryApi;
|
|
using Umbraco.Cms.Core.Models.PublishedContent;
|
|
using Umbraco.Cms.Core.PublishedCache;
|
|
|
|
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
|
|
|
[TestFixture]
|
|
public class PublishedContentCacheTests : DeliveryApiTests
|
|
{
|
|
private readonly Guid _contentOneId = Guid.Parse("19AEAC73-DB4E-4CFC-AB06-0AD14A89A613");
|
|
|
|
private readonly Guid _contentTwoId = Guid.Parse("4EF11E1E-FB50-4627-8A86-E10ED6F4DCE4");
|
|
|
|
private IPublishedSnapshotAccessor _publishedSnapshotAccessor = null!;
|
|
|
|
[SetUp]
|
|
public void Setup()
|
|
{
|
|
var contentTypeOneMock = new Mock<IPublishedContentType>();
|
|
contentTypeOneMock.SetupGet(m => m.Alias).Returns("theContentType");
|
|
var contentOneMock = new Mock<IPublishedContent>();
|
|
ConfigurePublishedContentMock(contentOneMock, _contentOneId, "Content One", "content-one", contentTypeOneMock.Object, Array.Empty<IPublishedProperty>());
|
|
|
|
var contentTypeTwoMock = new Mock<IPublishedContentType>();
|
|
contentTypeTwoMock.SetupGet(m => m.Alias).Returns("theOtherContentType");
|
|
var contentTwoMock = new Mock<IPublishedContent>();
|
|
ConfigurePublishedContentMock(contentTwoMock, _contentTwoId, "Content Two", "content-two", contentTypeTwoMock.Object, Array.Empty<IPublishedProperty>());
|
|
|
|
var contentCacheMock = new Mock<IPublishedContentCache>();
|
|
contentCacheMock
|
|
.Setup(m => m.GetByRoute(It.IsAny<bool>(), "content-one", null, null))
|
|
.Returns(contentOneMock.Object);
|
|
contentCacheMock
|
|
.Setup(m => m.GetById(It.IsAny<bool>(), _contentOneId))
|
|
.Returns(contentOneMock.Object);
|
|
contentCacheMock
|
|
.Setup(m => m.GetByRoute(It.IsAny<bool>(), "content-two", null, null))
|
|
.Returns(contentTwoMock.Object);
|
|
contentCacheMock
|
|
.Setup(m => m.GetById(It.IsAny<bool>(), _contentTwoId))
|
|
.Returns(contentTwoMock.Object);
|
|
|
|
var publishedSnapshotMock = new Mock<IPublishedSnapshot>();
|
|
publishedSnapshotMock.Setup(m => m.Content).Returns(contentCacheMock.Object);
|
|
|
|
var publishedSnapshot = publishedSnapshotMock.Object;
|
|
var publishedSnapshotAccessorMock = new Mock<IPublishedSnapshotAccessor>();
|
|
publishedSnapshotAccessorMock.Setup(m => m.TryGetPublishedSnapshot(out publishedSnapshot)).Returns(true);
|
|
|
|
_publishedSnapshotAccessor = publishedSnapshotAccessorMock.Object;
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_CanGetById()
|
|
{
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings());
|
|
var content = publishedContentCache.GetById(_contentOneId);
|
|
Assert.IsNotNull(content);
|
|
Assert.AreEqual(_contentOneId, content.Key);
|
|
Assert.AreEqual("content-one", content.UrlSegment);
|
|
Assert.AreEqual("theContentType", content.ContentType.Alias);
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_CanGetByRoute()
|
|
{
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings());
|
|
var content = publishedContentCache.GetByRoute("content-two");
|
|
Assert.IsNotNull(content);
|
|
Assert.AreEqual(_contentTwoId, content.Key);
|
|
Assert.AreEqual("content-two", content.UrlSegment);
|
|
Assert.AreEqual("theOtherContentType", content.ContentType.Alias);
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_CanGetByIds()
|
|
{
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings());
|
|
var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray();
|
|
Assert.AreEqual(2, content.Length);
|
|
Assert.AreEqual(_contentOneId, content.First().Key);
|
|
Assert.AreEqual(_contentTwoId, content.Last().Key);
|
|
}
|
|
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public void PublishedContentCache_GetById_SupportsDenyList(bool denied)
|
|
{
|
|
var denyList = denied ? new[] { "theOtherContentType" } : null;
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetById(_contentTwoId);
|
|
|
|
if (denied)
|
|
{
|
|
Assert.IsNull(content);
|
|
}
|
|
else
|
|
{
|
|
Assert.IsNotNull(content);
|
|
}
|
|
}
|
|
|
|
[TestCase(true)]
|
|
[TestCase(false)]
|
|
public void PublishedContentCache_GetByRoute_SupportsDenyList(bool denied)
|
|
{
|
|
var denyList = denied ? new[] { "theContentType" } : null;
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetByRoute("content-one");
|
|
|
|
if (denied)
|
|
{
|
|
Assert.IsNull(content);
|
|
}
|
|
else
|
|
{
|
|
Assert.IsNotNull(content);
|
|
}
|
|
}
|
|
|
|
[TestCase("theContentType")]
|
|
[TestCase("theOtherContentType")]
|
|
public void PublishedContentCache_GetByIds_SupportsDenyList(string deniedContentType)
|
|
{
|
|
var denyList = new[] { deniedContentType };
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray();
|
|
|
|
Assert.AreEqual(1, content.Length);
|
|
if (deniedContentType == "theContentType")
|
|
{
|
|
Assert.AreEqual(_contentTwoId, content.First().Key);
|
|
}
|
|
else
|
|
{
|
|
Assert.AreEqual(_contentOneId, content.First().Key);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_GetById_CanRetrieveContentTypesOutsideTheDenyList()
|
|
{
|
|
var denyList = new[] { "theContentType" };
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetById(_contentTwoId);
|
|
Assert.IsNotNull(content);
|
|
Assert.AreEqual(_contentTwoId, content.Key);
|
|
Assert.AreEqual("content-two", content.UrlSegment);
|
|
Assert.AreEqual("theOtherContentType", content.ContentType.Alias);
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_GetByRoute_CanRetrieveContentTypesOutsideTheDenyList()
|
|
{
|
|
var denyList = new[] { "theOtherContentType" };
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetByRoute("content-one");
|
|
Assert.IsNotNull(content);
|
|
Assert.AreEqual(_contentOneId, content.Key);
|
|
Assert.AreEqual("content-one", content.UrlSegment);
|
|
Assert.AreEqual("theContentType", content.ContentType.Alias);
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_GetByIds_CanDenyAllRequestedContent()
|
|
{
|
|
var denyList = new[] { "theContentType", "theOtherContentType" };
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray();
|
|
Assert.IsEmpty(content);
|
|
}
|
|
|
|
[Test]
|
|
public void PublishedContentCache_DenyListIsCaseInsensitive()
|
|
{
|
|
var denyList = new[] { "THEcontentTYPE" };
|
|
var publishedContentCache = new ApiPublishedContentCache(_publishedSnapshotAccessor, CreateRequestPreviewService(), CreateDeliveryApiSettings(denyList));
|
|
var content = publishedContentCache.GetByRoute("content-one");
|
|
Assert.IsNull(content);
|
|
}
|
|
|
|
private IRequestPreviewService CreateRequestPreviewService(bool isPreview = false)
|
|
{
|
|
var previewServiceMock = new Mock<IRequestPreviewService>();
|
|
previewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview);
|
|
return previewServiceMock.Object;
|
|
}
|
|
|
|
private IOptionsMonitor<DeliveryApiSettings> CreateDeliveryApiSettings(string[]? disallowedContentTypeAliases = null)
|
|
{
|
|
var deliveryApiSettings = new DeliveryApiSettings { DisallowedContentTypeAliases = disallowedContentTypeAliases ?? Array.Empty<string>() };
|
|
var deliveryApiOptionsMonitorMock = new Mock<IOptionsMonitor<DeliveryApiSettings>>();
|
|
deliveryApiOptionsMonitorMock.SetupGet(s => s.CurrentValue).Returns(deliveryApiSettings);
|
|
return deliveryApiOptionsMonitorMock.Object;
|
|
}
|
|
}
|