Merge remote-tracking branch 'origin/release/13.0' into v13/dev
This commit is contained in:
@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Services;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
@@ -48,18 +49,31 @@ public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _requestMemberAccessService = requestMemberAccessService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the content item.</param>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the content item.</param>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByIdV20(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(Guid id)
|
||||
{
|
||||
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdsContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
@@ -49,17 +50,29 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _requestMemberAccessService = requestMemberAccessService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets content items by ids.
|
||||
/// </summary>
|
||||
/// <param name="ids">The unique identifiers of the content items to retrieve.</param>
|
||||
/// <returns>The content items.</returns>
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
/// <summary>
|
||||
/// Gets content items by ids.
|
||||
/// </summary>
|
||||
/// <param name="ids">The unique identifiers of the content items to retrieve.</param>
|
||||
/// <returns>The content items.</returns>
|
||||
[HttpGet("items")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> ItemsV20([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(HashSet<Guid> ids)
|
||||
{
|
||||
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestRoutingService _requestRoutingService;
|
||||
@@ -73,6 +74,16 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
}
|
||||
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ByRoute(string path = "")
|
||||
=> await HandleRequest(path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by route.
|
||||
/// </summary>
|
||||
@@ -83,12 +94,15 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
/// </remarks>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByRoute(string path = "")
|
||||
public async Task<IActionResult> ByRouteV20(string path = "")
|
||||
=> await HandleRequest(path);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string path)
|
||||
{
|
||||
path = DecodePath(path);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class QueryContentApiController : ContentApiControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
@@ -45,6 +46,20 @@ public class QueryContentApiController : ContentApiControllerBase
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of content item(s) from query.
|
||||
/// </summary>
|
||||
@@ -55,16 +70,19 @@ public class QueryContentApiController : ContentApiControllerBase
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the content item(s).</returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Query(
|
||||
public async Task<IActionResult> QueryV20(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string? fetch, string[] filter, string[] sort, int skip, int take)
|
||||
{
|
||||
ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
@@ -16,16 +17,27 @@ public class ByIdMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a media item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the media item.</param>
|
||||
/// <returns>The media item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
public async Task<IActionResult> ByIdV20(Guid id)
|
||||
=> await HandleRequest(id);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(Guid id)
|
||||
{
|
||||
IPublishedContent? media = PublishedMediaCache.GetById(id);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByIdsMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
public ByIdsMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
@@ -17,15 +18,25 @@ public class ByIdsMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
/// <summary>
|
||||
/// Gets media items by ids.
|
||||
/// </summary>
|
||||
/// <param name="ids">The unique identifiers of the media items to retrieve.</param>
|
||||
/// <returns>The media items.</returns>
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("items")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
public async Task<IActionResult> ItemsV20([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
=> await HandleRequest(ids);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(HashSet<Guid> ids)
|
||||
{
|
||||
IPublishedContent[] mediaItems = ids
|
||||
.Select(PublishedMediaCache.GetById)
|
||||
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class ByPathMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaQueryService _apiMediaQueryService;
|
||||
@@ -21,16 +22,27 @@ public class ByPathMediaApiController : MediaApiControllerBase
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
=> _apiMediaQueryService = apiMediaQueryService;
|
||||
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> ByPath(string path)
|
||||
=> await HandleRequest(path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a media item by its path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the media item.</param>
|
||||
/// <returns>The media item or not found result.</returns>
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(IApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByPath(string path)
|
||||
public async Task<IActionResult> ByPathV20(string path)
|
||||
=> await HandleRequest(path);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string path)
|
||||
{
|
||||
path = DecodePath(path);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("2.0")]
|
||||
public class QueryMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaQueryService _apiMediaQueryService;
|
||||
@@ -26,6 +27,19 @@ public class QueryMediaApiController : MediaApiControllerBase
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
=> _apiMediaQueryService = apiMediaQueryService;
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[Obsolete("Please use version 2 of this API. Will be removed in V15.")]
|
||||
public async Task<IActionResult> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of media item(s) from query.
|
||||
/// </summary>
|
||||
@@ -36,15 +50,18 @@ public class QueryMediaApiController : MediaApiControllerBase
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the media item(s).</returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[MapToApiVersion("2.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Query(
|
||||
public async Task<IActionResult> QueryV20(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
=> await HandleRequest(fetch, filter, sort, skip, take);
|
||||
|
||||
private async Task<IActionResult> HandleRequest(string? fetch, string[] filter, string[] sort, int skip, int take)
|
||||
{
|
||||
Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> queryAttempt = _apiMediaQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
@@ -24,7 +26,22 @@ public static class UmbracoBuilderExtensions
|
||||
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
|
||||
builder.Services.AddScoped<IOutputExpansionStrategy, RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
|
||||
builder.Services.AddScoped<IOutputExpansionStrategy>(provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
}
|
||||
|
||||
// V1 of the Delivery API uses a different expansion strategy than V2+
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
});
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
|
||||
@@ -15,7 +15,9 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
{
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation);
|
||||
AddExpand(operation, context);
|
||||
|
||||
AddFields(operation, context);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
|
||||
@@ -37,8 +37,52 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
parameter.Examples = examples;
|
||||
}
|
||||
|
||||
protected void AddExpand(OpenApiOperation operation) =>
|
||||
protected void AddExpand(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (IsApiV1(context))
|
||||
{
|
||||
AddExpandV1(operation);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddExpand(operation);
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddFields(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (IsApiV1(context))
|
||||
{
|
||||
// "fields" is not a thing in Delivery API V1
|
||||
return;
|
||||
}
|
||||
|
||||
AddFields(operation);
|
||||
}
|
||||
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private static bool IsApiV1(OperationFilterContext context)
|
||||
=> context.ApiDescription.RelativePath?.Contains("api/v1") is true;
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private void AddExpandV1(OpenApiOperation operation)
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
@@ -60,19 +104,56 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
}
|
||||
});
|
||||
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
private void AddExpand(OpenApiOperation operation)
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
In = ParameterLocation.Header,
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Expand all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
|
||||
{
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
|
||||
},
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
|
||||
},
|
||||
{
|
||||
"Expand nested properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
private void AddFields(OpenApiOperation operation)
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "fields",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Include all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
|
||||
{
|
||||
"Include only specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
|
||||
},
|
||||
{
|
||||
"Include only specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
|
||||
},
|
||||
{
|
||||
"Include only specific nested properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
|
||||
{
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation);
|
||||
AddExpand(operation, context);
|
||||
|
||||
AddFields(operation, context);
|
||||
|
||||
AddApiKey(operation);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Rendering;
|
||||
|
||||
internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
|
||||
{
|
||||
private const string All = "$all";
|
||||
private const string None = "";
|
||||
private const string ExpandParameterName = "expand";
|
||||
private const string FieldsParameterName = "fields";
|
||||
|
||||
private readonly IApiPropertyRenderer _propertyRenderer;
|
||||
private readonly ILogger<RequestContextOutputExpansionStrategyV2> _logger;
|
||||
|
||||
private readonly Stack<Node?> _expandProperties;
|
||||
private readonly Stack<Node?> _includeProperties;
|
||||
|
||||
public RequestContextOutputExpansionStrategyV2(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IApiPropertyRenderer propertyRenderer,
|
||||
ILogger<RequestContextOutputExpansionStrategyV2> logger)
|
||||
{
|
||||
_propertyRenderer = propertyRenderer;
|
||||
_logger = logger;
|
||||
_expandProperties = new Stack<Node?>();
|
||||
_includeProperties = new Stack<Node?>();
|
||||
|
||||
InitializeExpandAndInclude(httpContextAccessor);
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> content.ItemType == PublishedItemType.Content
|
||||
? MapProperties(content.Properties)
|
||||
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
|
||||
|
||||
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
|
||||
{
|
||||
if (media.ItemType != PublishedItemType.Media)
|
||||
{
|
||||
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
|
||||
}
|
||||
|
||||
IPublishedProperty[] properties = media
|
||||
.Properties
|
||||
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
|
||||
.ToArray();
|
||||
|
||||
return properties.Any()
|
||||
? MapProperties(properties)
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties, true);
|
||||
|
||||
private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
|
||||
|
||||
var toExpand = QueryValue(ExpandParameterName) ?? None;
|
||||
var toInclude = QueryValue(FieldsParameterName) ?? All;
|
||||
|
||||
try
|
||||
{
|
||||
_expandProperties.Push(Node.Parse(toExpand));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Could not parse the '{ExpandParameterName}' parameter. See exception for details.");
|
||||
throw new ArgumentException($"Could not parse the '{ExpandParameterName}' parameter: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_includeProperties.Push(Node.Parse(toInclude));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Could not parse the '{FieldsParameterName}' parameter. See exception for details.");
|
||||
throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
|
||||
{
|
||||
Node? currentExpandProperties = _expandProperties.Peek();
|
||||
if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
Node? currentIncludeProperties = _includeProperties.Peek();
|
||||
var result = new Dictionary<string, object?>();
|
||||
foreach (IPublishedProperty property in properties)
|
||||
{
|
||||
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
|
||||
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
|
||||
|
||||
_includeProperties.Push(nextIncludeProperties);
|
||||
_expandProperties.Push(nextExpandProperties);
|
||||
|
||||
result[property.Alias] = GetPropertyValue(property);
|
||||
|
||||
_expandProperties.Pop();
|
||||
_includeProperties.Pop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
|
||||
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
|
||||
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
|
||||
|
||||
private object? GetPropertyValue(IPublishedProperty property)
|
||||
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
|
||||
|
||||
private class Node
|
||||
{
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
|
||||
public List<Node> Items { get; } = new();
|
||||
|
||||
public static Node Parse(string value)
|
||||
{
|
||||
// verify that there are as many start brackets as there are end brackets
|
||||
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
|
||||
{
|
||||
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
|
||||
}
|
||||
|
||||
// verify that the value does not start with a start bracket
|
||||
if (value.StartsWith("["))
|
||||
{
|
||||
throw new ArgumentException("Value cannot start with a bracket");
|
||||
}
|
||||
|
||||
// verify that there are no empty brackets
|
||||
if (value.Contains("[]"))
|
||||
{
|
||||
throw new ArgumentException("Value cannot contain empty brackets");
|
||||
}
|
||||
|
||||
var stack = new Stack<Node>();
|
||||
var root = new Node { Key = "root" };
|
||||
stack.Push(root);
|
||||
|
||||
var currentNode = new Node();
|
||||
root.Items.Add(currentNode);
|
||||
|
||||
foreach (char c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '[': // Start a new node, child of the current node
|
||||
stack.Push(currentNode);
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ',': // Start a new node, but at the same level of the current node
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ']': // Back to parent of the current node
|
||||
currentNode = stack.Pop();
|
||||
break;
|
||||
default: // Add char to current node key
|
||||
currentNode.Key += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -29,9 +30,25 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
|
||||
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
|
||||
{
|
||||
var hasIdQueryParameter = httpContext.Request.Query.ContainsKey("id");
|
||||
ApiVersion? requestedApiVersion = httpContext.GetRequestedApiVersion();
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
ControllerActionDescriptor? controllerActionDescriptor = candidates[i].Endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
|
||||
CandidateState candidate = candidates[i];
|
||||
Endpoint? endpoint = candidate.Endpoint;
|
||||
|
||||
// NOTE: nullability for the CandidateState.Endpoint property is not correct - it *can* be null
|
||||
if (endpoint is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EndpointSupportsApiVersion(endpoint, requestedApiVersion) is false)
|
||||
{
|
||||
candidates.SetValidity(i, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
ControllerActionDescriptor? controllerActionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
|
||||
if (IsByIdsController(controllerActionDescriptor))
|
||||
{
|
||||
candidates.SetValidity(i, hasIdQueryParameter);
|
||||
@@ -45,6 +62,15 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool EndpointSupportsApiVersion(Endpoint endpoint, ApiVersion? requestedApiVersion)
|
||||
{
|
||||
ApiVersion[]? supportedApiVersions = endpoint.Metadata.GetMetadata<MapToApiVersionAttribute>()?.Versions.ToArray();
|
||||
|
||||
// if the endpoint is versioned, the requested API version must be among the API versions supported by the endpoint.
|
||||
// if the endpoint is NOT versioned, it cannot be used with a requested API version
|
||||
return supportedApiVersions?.Contains(requestedApiVersion) ?? requestedApiVersion is null;
|
||||
}
|
||||
|
||||
private static bool IsByIdsController(ControllerActionDescriptor? controllerActionDescriptor)
|
||||
=> IsControllerType<ByIdsContentApiController>(controllerActionDescriptor) || IsControllerType<ByIdsMediaApiController>(controllerActionDescriptor);
|
||||
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -419,7 +419,7 @@ public static class StringExtensions
|
||||
/// returns <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull("defaultValue")]
|
||||
public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) =>
|
||||
str.IsNullOrWhiteSpace() ? defaultValue : str;
|
||||
@@ -1557,4 +1557,9 @@ public static class StringExtensions
|
||||
|
||||
yield return sb.ToString();
|
||||
}
|
||||
|
||||
// having benchmarked various solutions (incl. for/foreach, split and LINQ based ones),
|
||||
// this is by far the fastest way to find string needles in a string haystack
|
||||
public static int CountOccurrences(this string haystack, string needle)
|
||||
=> haystack.Length - haystack.Replace(needle, string.Empty).Length;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ public interface IPublishedPropertyType
|
||||
/// </summary>
|
||||
PropertyCacheLevel DeliveryApiCacheLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property cache level for Delivery API representation when expanding the property.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to the value of <see cref="DeliveryApiCacheLevel"/>.</remarks>
|
||||
PropertyCacheLevel DeliveryApiCacheLevelForExpansion => DeliveryApiCacheLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property model CLR type.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
|
||||
private IPropertyValueConverter? _converter;
|
||||
private PropertyCacheLevel _cacheLevel;
|
||||
private PropertyCacheLevel _deliveryApiCacheLevel;
|
||||
private PropertyCacheLevel _deliveryApiCacheLevelForExpansion;
|
||||
|
||||
private Type? _modelClrType;
|
||||
private Type? _clrType;
|
||||
@@ -192,9 +193,15 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
|
||||
}
|
||||
|
||||
_cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Snapshot;
|
||||
_deliveryApiCacheLevel = _converter is IDeliveryApiPropertyValueConverter deliveryApiPropertyValueConverter
|
||||
? deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevel(this)
|
||||
: _cacheLevel;
|
||||
if (_converter is IDeliveryApiPropertyValueConverter deliveryApiPropertyValueConverter)
|
||||
{
|
||||
_deliveryApiCacheLevel = deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevel(this);
|
||||
_deliveryApiCacheLevelForExpansion = deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevelForExpansion(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deliveryApiCacheLevel = _deliveryApiCacheLevelForExpansion = _cacheLevel;
|
||||
}
|
||||
_modelClrType = _converter?.GetPropertyValueType(this) ?? typeof(object);
|
||||
}
|
||||
|
||||
@@ -244,6 +251,20 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel DeliveryApiCacheLevelForExpansion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
return _deliveryApiCacheLevelForExpansion;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? ConvertSourceToInter(IPublishedElement owner, object? source, bool preview)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,15 @@ public interface IDeliveryApiPropertyValueConverter : IPropertyValueConverter
|
||||
/// <returns>The property cache level.</returns>
|
||||
PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property cache level for Delivery API representation when expanding the property.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>The property cache level.</returns>
|
||||
/// <remarks>Defaults to the value of <see cref="GetDeliveryApiPropertyCacheLevel"/>.</remarks>
|
||||
PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType)
|
||||
=> GetDeliveryApiPropertyCacheLevel(propertyType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of values returned by the converter for Delivery API representation.
|
||||
/// </summary>
|
||||
|
||||
@@ -99,6 +99,8 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IApiContent);
|
||||
|
||||
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
|
||||
|
||||
@@ -124,6 +124,8 @@ public class MediaPickerValueConverter : PropertyValueConverterBase, IDeliveryAp
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable<IApiMedia>);
|
||||
|
||||
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
|
||||
|
||||
@@ -183,6 +183,8 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> GetEntityType(propertyType) switch
|
||||
{
|
||||
|
||||
@@ -89,6 +89,9 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
private void GetDeliveryApiCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel)
|
||||
=> GetCacheLevels(PropertyType.DeliveryApiCacheLevel, out cacheLevel, out referenceCacheLevel);
|
||||
|
||||
private void GetDeliveryApiCacheLevelsForExpansion(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel)
|
||||
=> GetCacheLevels(PropertyType.DeliveryApiCacheLevelForExpansion, out cacheLevel, out referenceCacheLevel);
|
||||
|
||||
private void GetCacheLevels(PropertyCacheLevel propertyTypeCacheLevel, out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel)
|
||||
{
|
||||
// based upon the current reference cache level (ReferenceCacheLevel) and this property
|
||||
@@ -223,7 +226,15 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase
|
||||
|
||||
public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null)
|
||||
{
|
||||
GetDeliveryApiCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
|
||||
PropertyCacheLevel cacheLevel, referenceCacheLevel;
|
||||
if (expanding)
|
||||
{
|
||||
GetDeliveryApiCacheLevelsForExpansion(out cacheLevel, out referenceCacheLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
GetDeliveryApiCacheLevels(out cacheLevel, out referenceCacheLevel);
|
||||
}
|
||||
|
||||
lock (_locko)
|
||||
{
|
||||
|
||||
@@ -52,6 +52,10 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof(BlockGridModel);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
=> ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview);
|
||||
@@ -59,6 +63,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof(ApiBlockGridModel);
|
||||
|
||||
@@ -111,6 +111,9 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof(ApiBlockListModel);
|
||||
|
||||
@@ -151,6 +151,8 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable<IApiMediaWithCrops>);
|
||||
|
||||
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
|
||||
|
||||
@@ -144,6 +144,8 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements;
|
||||
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> _deliveryApiSettings.RichTextOutputAsJson
|
||||
? typeof(IRichTextElement)
|
||||
|
||||
@@ -323,7 +323,7 @@ internal class Property : PublishedPropertyBase
|
||||
object? value;
|
||||
lock (_locko)
|
||||
{
|
||||
CacheValue cacheValues = GetCacheValues(PropertyType.DeliveryApiCacheLevel).For(culture, segment);
|
||||
CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment);
|
||||
|
||||
// initial reference cache level always is .Content
|
||||
const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element;
|
||||
|
||||
@@ -49,6 +49,7 @@ public class CacheTests
|
||||
var invocationCount = 0;
|
||||
propertyType.SetupGet(p => p.CacheLevel).Returns(cacheLevel);
|
||||
propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(cacheLevel);
|
||||
propertyType.SetupGet(p => p.DeliveryApiCacheLevelForExpansion).Returns(cacheLevel);
|
||||
propertyType
|
||||
.Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny<IPublishedElement>(), It.IsAny<PropertyCacheLevel>(), It.IsAny<object?>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
.Returns(() => $"Delivery API value: {++invocationCount}");
|
||||
|
||||
@@ -33,6 +33,7 @@ public class CacheTests : DeliveryApiTests
|
||||
propertyValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
propertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(cacheLevel);
|
||||
propertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(cacheLevel);
|
||||
propertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevelForExpansion(It.IsAny<IPublishedPropertyType>())).Returns(cacheLevel);
|
||||
|
||||
var propertyType = SetupPublishedPropertyType(propertyValueConverter.Object, "something", "Some.Thing");
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ public class DeliveryApiTests
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
deliveryApiPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevelForExpansion(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
|
||||
DeliveryApiPropertyType = SetupPublishedPropertyType(deliveryApiPropertyValueConverter.Object, "deliveryApi", "Delivery.Api.Editor");
|
||||
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// The tests contained within this class all serve to test property expansion V1 and V2 exactly the same.
|
||||
/// </summary>
|
||||
public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTests
|
||||
{
|
||||
private IPublishedContentType _contentType;
|
||||
private IPublishedContentType _elementType;
|
||||
private IPublishedContentType _mediaType;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var contentType = new Mock<IPublishedContentType>();
|
||||
contentType.SetupGet(c => c.Alias).Returns("thePageType");
|
||||
contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content);
|
||||
_contentType = contentType.Object;
|
||||
var elementType = new Mock<IPublishedContentType>();
|
||||
elementType.SetupGet(c => c.Alias).Returns("theElementType");
|
||||
elementType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Element);
|
||||
_elementType = elementType.Object;
|
||||
var mediaType = new Mock<IPublishedContentType>();
|
||||
mediaType.SetupGet(c => c.Alias).Returns("theMediaType");
|
||||
mediaType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Media);
|
||||
_mediaType = mediaType.Object;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_ExpandsNothingByDefault()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false);
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None);
|
||||
var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None);
|
||||
|
||||
var contentPickerContent = CreateSimplePickedContent(123, 456);
|
||||
var contentPickerProperty = CreateContentPickerProperty(content.Object, contentPickerContent.Key, "contentPicker", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, prop1, prop2, contentPickerProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(3, result.Properties.Count);
|
||||
Assert.AreEqual("Delivery API value", result.Properties[DeliveryApiPropertyType.Alias]);
|
||||
Assert.AreEqual("Default value", result.Properties[DefaultPropertyType.Alias]);
|
||||
var contentPickerOutput = result.Properties["contentPicker"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(contentPickerContent.Key, contentPickerOutput.Id);
|
||||
Assert.IsEmpty(contentPickerOutput.Properties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandSpecificContent()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "contentPickerTwo" });
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var contentPickerOneContent = CreateSimplePickedContent(12, 34);
|
||||
var contentPickerOneProperty = CreateContentPickerProperty(content.Object, contentPickerOneContent.Key, "contentPickerOne", apiContentBuilder);
|
||||
var contentPickerTwoContent = CreateSimplePickedContent(56, 78);
|
||||
var contentPickerTwoProperty = CreateContentPickerProperty(content.Object, contentPickerTwoContent.Key, "contentPickerTwo", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerOneProperty, contentPickerTwoProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties["contentPickerOne"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerOneContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.IsEmpty(contentPickerOneOutput.Properties);
|
||||
|
||||
var contentPickerTwoOutput = result.Properties["contentPickerTwo"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerTwoOutput);
|
||||
Assert.AreEqual(contentPickerTwoContent.Key, contentPickerTwoOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(56, contentPickerTwoOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(78, contentPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void OutputExpansionStrategy_CanExpandSpecificMedia(bool mediaPicker3)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "mediaPickerTwo" });
|
||||
var apiMediaBuilder = new ApiMediaBuilder(
|
||||
new ApiContentNameProvider(),
|
||||
new ApiMediaUrlProvider(PublishedUrlProvider),
|
||||
Mock.Of<IPublishedValueFallback>(),
|
||||
accessor);
|
||||
|
||||
var media = new Mock<IPublishedContent>();
|
||||
|
||||
var mediaPickerOneContent = CreateSimplePickedMedia(12, 34);
|
||||
var mediaPickerOneProperty = mediaPicker3
|
||||
? CreateMediaPicker3Property(media.Object, mediaPickerOneContent.Key, "mediaPickerOne", apiMediaBuilder)
|
||||
: CreateMediaPickerProperty(media.Object, mediaPickerOneContent.Key, "mediaPickerOne", apiMediaBuilder);
|
||||
var mediaPickerTwoContent = CreateSimplePickedMedia(56, 78);
|
||||
var mediaPickerTwoProperty = mediaPicker3
|
||||
? CreateMediaPicker3Property(media.Object, mediaPickerTwoContent.Key, "mediaPickerTwo", apiMediaBuilder)
|
||||
: CreateMediaPickerProperty(media.Object, mediaPickerTwoContent.Key, "mediaPickerTwo", apiMediaBuilder);
|
||||
|
||||
SetupMediaMock(media, mediaPickerOneProperty, mediaPickerTwoProperty);
|
||||
|
||||
var result = apiMediaBuilder.Build(media.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var mediaPickerOneOutput = (result.Properties["mediaPickerOne"] as IEnumerable<IApiMedia>)?.FirstOrDefault();
|
||||
Assert.IsNotNull(mediaPickerOneOutput);
|
||||
Assert.AreEqual(mediaPickerOneContent.Key, mediaPickerOneOutput.Id);
|
||||
Assert.IsEmpty(mediaPickerOneOutput.Properties);
|
||||
|
||||
var mediaPickerTwoOutput = (result.Properties["mediaPickerTwo"] as IEnumerable<IApiMedia>)?.FirstOrDefault();
|
||||
Assert.IsNotNull(mediaPickerTwoOutput);
|
||||
Assert.AreEqual(mediaPickerTwoContent.Key, mediaPickerTwoOutput.Id);
|
||||
Assert.AreEqual(2, mediaPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(56, mediaPickerTwoOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(78, mediaPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandAllContent()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(true);
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var contentPickerOneContent = CreateSimplePickedContent(12, 34);
|
||||
var contentPickerOneProperty = CreateContentPickerProperty(content.Object, contentPickerOneContent.Key, "contentPickerOne", apiContentBuilder);
|
||||
var contentPickerTwoContent = CreateSimplePickedContent(56, 78);
|
||||
var contentPickerTwoProperty = CreateContentPickerProperty(content.Object, contentPickerTwoContent.Key, "contentPickerTwo", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerOneProperty, contentPickerTwoProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties["contentPickerOne"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerOneContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOneOutput.Properties.Count);
|
||||
Assert.AreEqual(12, contentPickerOneOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(34, contentPickerOneOutput.Properties["numberTwo"]);
|
||||
|
||||
var contentPickerTwoOutput = result.Properties["contentPickerTwo"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerTwoOutput);
|
||||
Assert.AreEqual(contentPickerTwoContent.Key, contentPickerTwoOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(56, contentPickerTwoOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(78, contentPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[TestCase("contentPicker", "contentPicker")]
|
||||
[TestCase("rootPicker", "nestedPicker")]
|
||||
public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { rootPropertyTypeAlias, nestedPropertyTypeAlias });
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var nestedContentPickerContent = CreateSimplePickedContent(987, 654);
|
||||
var contentPickerContent = CreateMultiLevelPickedContent(123, nestedContentPickerContent, nestedPropertyTypeAlias, apiContentBuilder);
|
||||
var contentPickerContentProperty = CreateContentPickerProperty(content.Object, contentPickerContent.Key, rootPropertyTypeAlias, apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerContentProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties[rootPropertyTypeAlias] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOneOutput.Properties.Count);
|
||||
Assert.AreEqual(123, contentPickerOneOutput.Properties["number"]);
|
||||
|
||||
var nestedContentPickerOutput = contentPickerOneOutput.Properties[nestedPropertyTypeAlias] as ApiContent;
|
||||
Assert.IsNotNull(nestedContentPickerOutput);
|
||||
Assert.AreEqual(nestedContentPickerContent.Key, nestedContentPickerOutput.Id);
|
||||
Assert.IsEmpty(nestedContentPickerOutput.Properties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_DoesNotExpandElementsByDefault()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false);
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
var apiElementBuilder = new ApiElementBuilder(accessor);
|
||||
|
||||
var contentPickerValue = CreateSimplePickedContent(111, 222);
|
||||
var contentPicker2Value = CreateSimplePickedContent(666, 777);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, 444, "number"),
|
||||
CreateElementProperty(content.Object, "element", 333, contentPickerValue.Key, "contentPicker", apiContentBuilder, apiElementBuilder),
|
||||
CreateElementProperty(content.Object, "element2", 555, contentPicker2Value.Key, "contentPicker", apiContentBuilder, apiElementBuilder));
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(3, result.Properties.Count);
|
||||
Assert.AreEqual(444, result.Properties["number"]);
|
||||
|
||||
var expectedElementOutputs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
PropertyAlias = "element",
|
||||
ElementNumber = 333,
|
||||
ElementContentPicker = contentPickerValue.Key
|
||||
},
|
||||
new
|
||||
{
|
||||
PropertyAlias = "element2",
|
||||
ElementNumber = 555,
|
||||
ElementContentPicker = contentPicker2Value.Key
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var expectedElementOutput in expectedElementOutputs)
|
||||
{
|
||||
var elementOutput = result.Properties[expectedElementOutput.PropertyAlias] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(expectedElementOutput.ElementNumber, elementOutput.Properties["number"]);
|
||||
var contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(expectedElementOutput.ElementContentPicker, contentPickerOutput.Id);
|
||||
Assert.AreEqual(0, contentPickerOutput.Properties.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_MappingContent_ThrowsOnInvalidItemType()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false);
|
||||
if (accessor.TryGetValue(out IOutputExpansionStrategy outputExpansionStrategy) is false)
|
||||
{
|
||||
Assert.Fail("Could not obtain the output expansion strategy");
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => outputExpansionStrategy.MapContentProperties(PublishedMedia));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_MappingMedia_ThrowsOnInvalidItemType()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false);
|
||||
if (accessor.TryGetValue(out IOutputExpansionStrategy outputExpansionStrategy) is false)
|
||||
{
|
||||
Assert.Fail("Could not obtain the output expansion strategy");
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => outputExpansionStrategy.MapMediaProperties(PublishedContent));
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void OutputExpansionStrategy_ForwardsExpansionStateToPropertyValueConverter(bool expanding)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { expanding ? "theAlias" : "noSuchAlias" });
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var valueConverterMock = new Mock<IDeliveryApiPropertyValueConverter>();
|
||||
valueConverterMock.Setup(v => v.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
valueConverterMock.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
valueConverterMock.Setup(v => v.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevelForExpansion(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
valueConverterMock.Setup(v => v.ConvertIntermediateToDeliveryApiObject(
|
||||
It.IsAny<IPublishedElement>(),
|
||||
It.IsAny<IPublishedPropertyType>(),
|
||||
It.IsAny<PropertyCacheLevel>(),
|
||||
It.IsAny<object?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(expanding ? "Expanding" : "Not expanding");
|
||||
|
||||
var propertyType = SetupPublishedPropertyType(valueConverterMock.Object, "theAlias", Constants.PropertyEditors.Aliases.Label);
|
||||
var property = new PublishedElementPropertyBase(propertyType, content.Object, false, PropertyCacheLevel.None, "The Value");
|
||||
|
||||
SetupContentMock(content, property);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
Assert.AreEqual(expanding ? "Expanding" : "Not expanding", result.Properties["theAlias"] as string);
|
||||
}
|
||||
|
||||
protected abstract IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor(string? expand = null, string? fields = null);
|
||||
|
||||
protected IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor(bool expandAll = false, string[]? expandPropertyAliases = null)
|
||||
=> CreateOutputExpansionStrategyAccessor(FormatExpandSyntax(expandAll, expandPropertyAliases));
|
||||
|
||||
protected abstract string? FormatExpandSyntax(bool expandAll = false, string[]? expandPropertyAliases = null);
|
||||
|
||||
protected void SetupContentMock(Mock<IPublishedContent> content, params IPublishedProperty[] properties)
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
var name = "The page";
|
||||
var urlSegment = "url-segment";
|
||||
ConfigurePublishedContentMock(content, key, name, urlSegment, _contentType, properties);
|
||||
|
||||
RegisterContentWithProviders(content.Object);
|
||||
}
|
||||
|
||||
protected void SetupMediaMock(Mock<IPublishedContent> media, params IPublishedProperty[] properties)
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
var name = "The media";
|
||||
var urlSegment = "media-url-segment";
|
||||
ConfigurePublishedContentMock(media, key, name, urlSegment, _mediaType, properties);
|
||||
|
||||
RegisterMediaWithProviders(media.Object);
|
||||
}
|
||||
|
||||
protected IPublishedContent CreateSimplePickedContent(int numberOneValue, int numberTwoValue)
|
||||
{
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, numberOneValue, "numberOne"),
|
||||
CreateNumberProperty(content.Object, numberTwoValue, "numberTwo"));
|
||||
|
||||
return content.Object;
|
||||
}
|
||||
|
||||
protected IPublishedContent CreateSimplePickedMedia(int numberOneValue, int numberTwoValue)
|
||||
{
|
||||
var media = new Mock<IPublishedContent>();
|
||||
SetupMediaMock(
|
||||
media,
|
||||
CreateNumberProperty(media.Object, numberOneValue, "numberOne"),
|
||||
CreateNumberProperty(media.Object, numberTwoValue, "numberTwo"));
|
||||
|
||||
return media.Object;
|
||||
}
|
||||
|
||||
protected IPublishedContent CreateMultiLevelPickedContent(int numberValue, IPublishedContent nestedContentPickerValue, string nestedContentPickerPropertyTypeAlias, ApiContentBuilder apiContentBuilder)
|
||||
{
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, numberValue, "number"),
|
||||
CreateContentPickerProperty(content.Object, nestedContentPickerValue.Key, nestedContentPickerPropertyTypeAlias, apiContentBuilder));
|
||||
|
||||
return content.Object;
|
||||
}
|
||||
|
||||
internal PublishedElementPropertyBase CreateContentPickerProperty(IPublishedElement parent, Guid pickedContentKey, string propertyTypeAlias, IApiContentBuilder contentBuilder)
|
||||
{
|
||||
ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedSnapshotAccessor, contentBuilder);
|
||||
var contentPickerPropertyType = SetupPublishedPropertyType(contentPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.ContentPicker);
|
||||
|
||||
return new PublishedElementPropertyBase(contentPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Document, pickedContentKey).ToString());
|
||||
}
|
||||
|
||||
internal PublishedElementPropertyBase CreateMediaPickerProperty(IPublishedElement parent, Guid pickedMediaKey, string propertyTypeAlias, IApiMediaBuilder mediaBuilder)
|
||||
{
|
||||
MediaPickerValueConverter mediaPickerValueConverter = new MediaPickerValueConverter(PublishedSnapshotAccessor, Mock.Of<IPublishedModelFactory>(), mediaBuilder);
|
||||
var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker, new MediaPickerConfiguration());
|
||||
|
||||
return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Media, pickedMediaKey).ToString());
|
||||
}
|
||||
|
||||
internal PublishedElementPropertyBase CreateMediaPicker3Property(IPublishedElement parent, Guid pickedMediaKey, string propertyTypeAlias, IApiMediaBuilder mediaBuilder)
|
||||
{
|
||||
var serializer = new JsonNetSerializer();
|
||||
var value = serializer.Serialize(new[]
|
||||
{
|
||||
new MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.MediaWithCropsDto
|
||||
{
|
||||
MediaKey = pickedMediaKey
|
||||
}
|
||||
});
|
||||
|
||||
var publishedValueFallback = Mock.Of<IPublishedValueFallback>();
|
||||
var apiMediaWithCropsBuilder = new ApiMediaWithCropsBuilder(mediaBuilder, publishedValueFallback);
|
||||
|
||||
MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(PublishedSnapshotAccessor, PublishedUrlProvider, publishedValueFallback, new JsonNetSerializer(), apiMediaWithCropsBuilder);
|
||||
var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker3, new MediaPicker3Configuration());
|
||||
|
||||
return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, value);
|
||||
}
|
||||
|
||||
internal PublishedElementPropertyBase CreateNumberProperty(IPublishedElement parent, int propertyValue, string propertyTypeAlias)
|
||||
{
|
||||
var numberPropertyType = SetupPublishedPropertyType(new IntegerValueConverter(), propertyTypeAlias, Constants.PropertyEditors.Aliases.Label);
|
||||
return new PublishedElementPropertyBase(numberPropertyType, parent, false, PropertyCacheLevel.None, propertyValue);
|
||||
}
|
||||
|
||||
internal PublishedElementPropertyBase CreateElementProperty(
|
||||
IPublishedElement parent,
|
||||
string elementPropertyAlias,
|
||||
int numberPropertyValue,
|
||||
Guid contentPickerPropertyValue,
|
||||
string contentPickerPropertyTypeAlias,
|
||||
IApiContentBuilder apiContentBuilder,
|
||||
IApiElementBuilder apiElementBuilder)
|
||||
{
|
||||
var element = new Mock<IPublishedElement>();
|
||||
element.SetupGet(c => c.ContentType).Returns(_elementType);
|
||||
element.SetupGet(c => c.Properties).Returns(new[]
|
||||
{
|
||||
CreateNumberProperty(element.Object, numberPropertyValue, "number"),
|
||||
CreateContentPickerProperty(element.Object, contentPickerPropertyValue, contentPickerPropertyTypeAlias, apiContentBuilder)
|
||||
});
|
||||
|
||||
var elementValueConverter = new Mock<IDeliveryApiPropertyValueConverter>();
|
||||
elementValueConverter
|
||||
.Setup(p => p.ConvertIntermediateToDeliveryApiObject(
|
||||
It.IsAny<IPublishedElement>(),
|
||||
It.IsAny<IPublishedPropertyType>(),
|
||||
It.IsAny<PropertyCacheLevel>(),
|
||||
It.IsAny<object?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(() => apiElementBuilder.Build(element.Object));
|
||||
elementValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
elementValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
elementValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevelForExpansion(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
|
||||
var elementPropertyType = SetupPublishedPropertyType(elementValueConverter.Object, elementPropertyAlias, "My.Element.Property");
|
||||
return new PublishedElementPropertyBase(elementPropertyType, parent, false, PropertyCacheLevel.None);
|
||||
}
|
||||
|
||||
protected IApiContentRouteBuilder ApiContentRouteBuilder() => CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings());
|
||||
}
|
||||
@@ -3,258 +3,19 @@ using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Api.Delivery.Rendering;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// Any tests contained within this class specifically test property expansion V1 and not V2. If the aim is to test both
|
||||
/// versions, please put the tests in the base class.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class OutputExpansionStrategyTests : PropertyValueConverterTests
|
||||
public class OutputExpansionStrategyTests : OutputExpansionStrategyTestBase
|
||||
{
|
||||
private IPublishedContentType _contentType;
|
||||
private IPublishedContentType _elementType;
|
||||
private IPublishedContentType _mediaType;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var contentType = new Mock<IPublishedContentType>();
|
||||
contentType.SetupGet(c => c.Alias).Returns("thePageType");
|
||||
contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content);
|
||||
_contentType = contentType.Object;
|
||||
var elementType = new Mock<IPublishedContentType>();
|
||||
elementType.SetupGet(c => c.Alias).Returns("theElementType");
|
||||
elementType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Element);
|
||||
_elementType = elementType.Object;
|
||||
var mediaType = new Mock<IPublishedContentType>();
|
||||
mediaType.SetupGet(c => c.Alias).Returns("theMediaType");
|
||||
mediaType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Media);
|
||||
_mediaType = mediaType.Object;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_ExpandsNothingByDefault()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor();
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None);
|
||||
var prop2 = new PublishedElementPropertyBase(DefaultPropertyType, content.Object, false, PropertyCacheLevel.None);
|
||||
|
||||
var contentPickerContent = CreateSimplePickedContent(123, 456);
|
||||
var contentPickerProperty = CreateContentPickerProperty(content.Object, contentPickerContent.Key, "contentPicker", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, prop1, prop2, contentPickerProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(3, result.Properties.Count);
|
||||
Assert.AreEqual("Delivery API value", result.Properties[DeliveryApiPropertyType.Alias]);
|
||||
Assert.AreEqual("Default value", result.Properties[DefaultPropertyType.Alias]);
|
||||
var contentPickerOutput = result.Properties["contentPicker"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(contentPickerContent.Key, contentPickerOutput.Id);
|
||||
Assert.IsEmpty(contentPickerOutput.Properties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandSpecificContent()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "contentPickerTwo" });
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var contentPickerOneContent = CreateSimplePickedContent(12, 34);
|
||||
var contentPickerOneProperty = CreateContentPickerProperty(content.Object, contentPickerOneContent.Key, "contentPickerOne", apiContentBuilder);
|
||||
var contentPickerTwoContent = CreateSimplePickedContent(56, 78);
|
||||
var contentPickerTwoProperty = CreateContentPickerProperty(content.Object, contentPickerTwoContent.Key, "contentPickerTwo", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerOneProperty, contentPickerTwoProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties["contentPickerOne"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerOneContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.IsEmpty(contentPickerOneOutput.Properties);
|
||||
|
||||
var contentPickerTwoOutput = result.Properties["contentPickerTwo"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerTwoOutput);
|
||||
Assert.AreEqual(contentPickerTwoContent.Key, contentPickerTwoOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(56, contentPickerTwoOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(78, contentPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void OutputExpansionStrategy_CanExpandSpecificMedia(bool mediaPicker3)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "mediaPickerTwo" });
|
||||
var apiMediaBuilder = new ApiMediaBuilder(
|
||||
new ApiContentNameProvider(),
|
||||
new ApiMediaUrlProvider(PublishedUrlProvider),
|
||||
Mock.Of<IPublishedValueFallback>(),
|
||||
accessor);
|
||||
|
||||
var media = new Mock<IPublishedContent>();
|
||||
|
||||
var mediaPickerOneContent = CreateSimplePickedMedia(12, 34);
|
||||
var mediaPickerOneProperty = mediaPicker3
|
||||
? CreateMediaPicker3Property(media.Object, mediaPickerOneContent.Key, "mediaPickerOne", apiMediaBuilder)
|
||||
: CreateMediaPickerProperty(media.Object, mediaPickerOneContent.Key, "mediaPickerOne", apiMediaBuilder);
|
||||
var mediaPickerTwoContent = CreateSimplePickedMedia(56, 78);
|
||||
var mediaPickerTwoProperty = mediaPicker3
|
||||
? CreateMediaPicker3Property(media.Object, mediaPickerTwoContent.Key, "mediaPickerTwo", apiMediaBuilder)
|
||||
: CreateMediaPickerProperty(media.Object, mediaPickerTwoContent.Key, "mediaPickerTwo", apiMediaBuilder);
|
||||
|
||||
SetupMediaMock(media, mediaPickerOneProperty, mediaPickerTwoProperty);
|
||||
|
||||
var result = apiMediaBuilder.Build(media.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var mediaPickerOneOutput = (result.Properties["mediaPickerOne"] as IEnumerable<IApiMedia>)?.FirstOrDefault();
|
||||
Assert.IsNotNull(mediaPickerOneOutput);
|
||||
Assert.AreEqual(mediaPickerOneContent.Key, mediaPickerOneOutput.Id);
|
||||
Assert.IsEmpty(mediaPickerOneOutput.Properties);
|
||||
|
||||
var mediaPickerTwoOutput = (result.Properties["mediaPickerTwo"] as IEnumerable<IApiMedia>)?.FirstOrDefault();
|
||||
Assert.IsNotNull(mediaPickerTwoOutput);
|
||||
Assert.AreEqual(mediaPickerTwoContent.Key, mediaPickerTwoOutput.Id);
|
||||
Assert.AreEqual(2, mediaPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(56, mediaPickerTwoOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(78, mediaPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandAllContent()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(true);
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var contentPickerOneContent = CreateSimplePickedContent(12, 34);
|
||||
var contentPickerOneProperty = CreateContentPickerProperty(content.Object, contentPickerOneContent.Key, "contentPickerOne", apiContentBuilder);
|
||||
var contentPickerTwoContent = CreateSimplePickedContent(56, 78);
|
||||
var contentPickerTwoProperty = CreateContentPickerProperty(content.Object, contentPickerTwoContent.Key, "contentPickerTwo", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerOneProperty, contentPickerTwoProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties["contentPickerOne"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerOneContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOneOutput.Properties.Count);
|
||||
Assert.AreEqual(12, contentPickerOneOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(34, contentPickerOneOutput.Properties["numberTwo"]);
|
||||
|
||||
var contentPickerTwoOutput = result.Properties["contentPickerTwo"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerTwoOutput);
|
||||
Assert.AreEqual(contentPickerTwoContent.Key, contentPickerTwoOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(56, contentPickerTwoOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(78, contentPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[TestCase("contentPicker", "contentPicker")]
|
||||
[TestCase("rootPicker", "nestedPicker")]
|
||||
public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { rootPropertyTypeAlias, nestedPropertyTypeAlias });
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var nestedContentPickerContent = CreateSimplePickedContent(987, 654);
|
||||
var contentPickerContent = CreateMultiLevelPickedContent(123, nestedContentPickerContent, nestedPropertyTypeAlias, apiContentBuilder);
|
||||
var contentPickerContentProperty = CreateContentPickerProperty(content.Object, contentPickerContent.Key, rootPropertyTypeAlias, apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerContentProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties[rootPropertyTypeAlias] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOneOutput.Properties.Count);
|
||||
Assert.AreEqual(123, contentPickerOneOutput.Properties["number"]);
|
||||
|
||||
var nestedContentPickerOutput = contentPickerOneOutput.Properties[nestedPropertyTypeAlias] as ApiContent;
|
||||
Assert.IsNotNull(nestedContentPickerOutput);
|
||||
Assert.AreEqual(nestedContentPickerContent.Key, nestedContentPickerOutput.Id);
|
||||
Assert.IsEmpty(nestedContentPickerOutput.Properties);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_DoesNotExpandElementsByDefault()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor();
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
var apiElementBuilder = new ApiElementBuilder(accessor);
|
||||
|
||||
var contentPickerValue = CreateSimplePickedContent(111, 222);
|
||||
var contentPicker2Value = CreateSimplePickedContent(666, 777);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, 444, "number"),
|
||||
CreateElementProperty(content.Object, "element", 333, contentPickerValue.Key, "contentPicker", apiContentBuilder, apiElementBuilder),
|
||||
CreateElementProperty(content.Object, "element2", 555, contentPicker2Value.Key, "contentPicker", apiContentBuilder, apiElementBuilder));
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(3, result.Properties.Count);
|
||||
Assert.AreEqual(444, result.Properties["number"]);
|
||||
|
||||
var expectedElementOutputs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
PropertyAlias = "element",
|
||||
ElementNumber = 333,
|
||||
ElementContentPicker = contentPickerValue.Key
|
||||
},
|
||||
new
|
||||
{
|
||||
PropertyAlias = "element2",
|
||||
ElementNumber = 555,
|
||||
ElementContentPicker = contentPicker2Value.Key
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var expectedElementOutput in expectedElementOutputs)
|
||||
{
|
||||
var elementOutput = result.Properties[expectedElementOutput.PropertyAlias] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(expectedElementOutput.ElementNumber, elementOutput.Properties["number"]);
|
||||
var contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(expectedElementOutput.ElementContentPicker, contentPickerOutput.Id);
|
||||
Assert.AreEqual(0, contentPickerOutput.Properties.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandSpecifiedElement()
|
||||
{
|
||||
@@ -387,71 +148,12 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests
|
||||
Assert.AreEqual(0, nestedContentPickerOutput.Properties.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_MappingContent_ThrowsOnInvalidItemType()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor();
|
||||
if (accessor.TryGetValue(out IOutputExpansionStrategy outputExpansionStrategy) is false)
|
||||
{
|
||||
Assert.Fail("Could not obtain the output expansion strategy");
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => outputExpansionStrategy.MapContentProperties(PublishedMedia));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_MappingMedia_ThrowsOnInvalidItemType()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor();
|
||||
if (accessor.TryGetValue(out IOutputExpansionStrategy outputExpansionStrategy) is false)
|
||||
{
|
||||
Assert.Fail("Could not obtain the output expansion strategy");
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => outputExpansionStrategy.MapMediaProperties(PublishedContent));
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void OutputExpansionStrategy_ForwardsExpansionStateToPropertyValueConverter(bool expanding)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { expanding ? "theAlias" : "noSuchAlias" });
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var valueConverterMock = new Mock<IDeliveryApiPropertyValueConverter>();
|
||||
valueConverterMock.Setup(v => v.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
valueConverterMock.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
valueConverterMock.Setup(v => v.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
valueConverterMock.Setup(v => v.ConvertIntermediateToDeliveryApiObject(
|
||||
It.IsAny<IPublishedElement>(),
|
||||
It.IsAny<IPublishedPropertyType>(),
|
||||
It.IsAny<PropertyCacheLevel>(),
|
||||
It.IsAny<object?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(expanding ? "Expanding" : "Not expanding");
|
||||
|
||||
var propertyType = SetupPublishedPropertyType(valueConverterMock.Object, "theAlias", Constants.PropertyEditors.Aliases.Label);
|
||||
var property = new PublishedElementPropertyBase(propertyType, content.Object, false, PropertyCacheLevel.None, "The Value");
|
||||
|
||||
SetupContentMock(content, property);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
Assert.AreEqual(expanding ? "Expanding" : "Not expanding", result.Properties["theAlias"] as string);
|
||||
}
|
||||
|
||||
private IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor(bool expandAll = false, string[]? expandPropertyAliases = null)
|
||||
protected override IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor(string? expand = null, string? fields = null)
|
||||
{
|
||||
var httpContextMock = new Mock<HttpContext>();
|
||||
var httpRequestMock = new Mock<HttpRequest>();
|
||||
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
|
||||
|
||||
var expand = expandAll ? "all" : expandPropertyAliases != null ? $"property:{string.Join(",", expandPropertyAliases)}" : null;
|
||||
httpRequestMock
|
||||
.SetupGet(r => r.Query)
|
||||
.Returns(new QueryCollection(new Dictionary<string, StringValues> { { "expand", expand } }));
|
||||
@@ -466,136 +168,6 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests
|
||||
return outputExpansionStrategyAccessorMock.Object;
|
||||
}
|
||||
|
||||
private void SetupContentMock(Mock<IPublishedContent> content, params IPublishedProperty[] properties)
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
var name = "The page";
|
||||
var urlSegment = "url-segment";
|
||||
ConfigurePublishedContentMock(content, key, name, urlSegment, _contentType, properties);
|
||||
|
||||
RegisterContentWithProviders(content.Object);
|
||||
}
|
||||
|
||||
private void SetupMediaMock(Mock<IPublishedContent> media, params IPublishedProperty[] properties)
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
var name = "The media";
|
||||
var urlSegment = "media-url-segment";
|
||||
ConfigurePublishedContentMock(media, key, name, urlSegment, _mediaType, properties);
|
||||
|
||||
RegisterMediaWithProviders(media.Object);
|
||||
}
|
||||
|
||||
private IPublishedContent CreateSimplePickedContent(int numberOneValue, int numberTwoValue)
|
||||
{
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, numberOneValue, "numberOne"),
|
||||
CreateNumberProperty(content.Object, numberTwoValue, "numberTwo"));
|
||||
|
||||
return content.Object;
|
||||
}
|
||||
|
||||
private IPublishedContent CreateSimplePickedMedia(int numberOneValue, int numberTwoValue)
|
||||
{
|
||||
var media = new Mock<IPublishedContent>();
|
||||
SetupMediaMock(
|
||||
media,
|
||||
CreateNumberProperty(media.Object, numberOneValue, "numberOne"),
|
||||
CreateNumberProperty(media.Object, numberTwoValue, "numberTwo"));
|
||||
|
||||
return media.Object;
|
||||
}
|
||||
|
||||
private IPublishedContent CreateMultiLevelPickedContent(int numberValue, IPublishedContent nestedContentPickerValue, string nestedContentPickerPropertyTypeAlias, ApiContentBuilder apiContentBuilder)
|
||||
{
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, numberValue, "number"),
|
||||
CreateContentPickerProperty(content.Object, nestedContentPickerValue.Key, nestedContentPickerPropertyTypeAlias, apiContentBuilder));
|
||||
|
||||
return content.Object;
|
||||
}
|
||||
|
||||
private PublishedElementPropertyBase CreateContentPickerProperty(IPublishedElement parent, Guid pickedContentKey, string propertyTypeAlias, IApiContentBuilder contentBuilder)
|
||||
{
|
||||
ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedSnapshotAccessor, contentBuilder);
|
||||
var contentPickerPropertyType = SetupPublishedPropertyType(contentPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.ContentPicker);
|
||||
|
||||
return new PublishedElementPropertyBase(contentPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Document, pickedContentKey).ToString());
|
||||
}
|
||||
|
||||
private PublishedElementPropertyBase CreateMediaPickerProperty(IPublishedElement parent, Guid pickedMediaKey, string propertyTypeAlias, IApiMediaBuilder mediaBuilder)
|
||||
{
|
||||
MediaPickerValueConverter mediaPickerValueConverter = new MediaPickerValueConverter(PublishedSnapshotAccessor, Mock.Of<IPublishedModelFactory>(), mediaBuilder);
|
||||
var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker, new MediaPickerConfiguration());
|
||||
|
||||
return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Media, pickedMediaKey).ToString());
|
||||
}
|
||||
|
||||
private PublishedElementPropertyBase CreateMediaPicker3Property(IPublishedElement parent, Guid pickedMediaKey, string propertyTypeAlias, IApiMediaBuilder mediaBuilder)
|
||||
{
|
||||
var serializer = new JsonNetSerializer();
|
||||
var value = serializer.Serialize(new[]
|
||||
{
|
||||
new MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.MediaWithCropsDto
|
||||
{
|
||||
MediaKey = pickedMediaKey
|
||||
}
|
||||
});
|
||||
|
||||
var publishedValueFallback = Mock.Of<IPublishedValueFallback>();
|
||||
var apiMediaWithCropsBuilder = new ApiMediaWithCropsBuilder(mediaBuilder, publishedValueFallback);
|
||||
|
||||
MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(PublishedSnapshotAccessor, PublishedUrlProvider, publishedValueFallback, new JsonNetSerializer(), apiMediaWithCropsBuilder);
|
||||
var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker3, new MediaPicker3Configuration());
|
||||
|
||||
return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, value);
|
||||
}
|
||||
|
||||
private PublishedElementPropertyBase CreateNumberProperty(IPublishedElement parent, int propertyValue, string propertyTypeAlias)
|
||||
{
|
||||
var numberPropertyType = SetupPublishedPropertyType(new IntegerValueConverter(), propertyTypeAlias, Constants.PropertyEditors.Aliases.Label);
|
||||
return new PublishedElementPropertyBase(numberPropertyType, parent, false, PropertyCacheLevel.None, propertyValue);
|
||||
}
|
||||
|
||||
private PublishedElementPropertyBase CreateElementProperty(
|
||||
IPublishedElement parent,
|
||||
string elementPropertyAlias,
|
||||
int numberPropertyValue,
|
||||
Guid contentPickerPropertyValue,
|
||||
string contentPickerPropertyTypeAlias,
|
||||
IApiContentBuilder apiContentBuilder,
|
||||
IApiElementBuilder apiElementBuilder)
|
||||
{
|
||||
var element = new Mock<IPublishedElement>();
|
||||
element.SetupGet(c => c.ContentType).Returns(_elementType);
|
||||
element.SetupGet(c => c.Properties).Returns(new[]
|
||||
{
|
||||
CreateNumberProperty(element.Object, numberPropertyValue, "number"),
|
||||
CreateContentPickerProperty(element.Object, contentPickerPropertyValue, contentPickerPropertyTypeAlias, apiContentBuilder)
|
||||
});
|
||||
|
||||
var elementValueConverter = new Mock<IDeliveryApiPropertyValueConverter>();
|
||||
elementValueConverter
|
||||
.Setup(p => p.ConvertIntermediateToDeliveryApiObject(
|
||||
It.IsAny<IPublishedElement>(),
|
||||
It.IsAny<IPublishedPropertyType>(),
|
||||
It.IsAny<PropertyCacheLevel>(),
|
||||
It.IsAny<object?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<bool>()))
|
||||
.Returns(() => apiElementBuilder.Build(element.Object));
|
||||
elementValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
|
||||
elementValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(true);
|
||||
elementValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
elementValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
|
||||
|
||||
var elementPropertyType = SetupPublishedPropertyType(elementValueConverter.Object, elementPropertyAlias, "My.Element.Property");
|
||||
return new PublishedElementPropertyBase(elementPropertyType, parent, false, PropertyCacheLevel.None);
|
||||
}
|
||||
|
||||
private IApiContentRouteBuilder ApiContentRouteBuilder() => CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings());
|
||||
protected override string? FormatExpandSyntax(bool expandAll = false, string[]? expandPropertyAliases = null)
|
||||
=> expandAll ? "all" : expandPropertyAliases?.Any() is true ? $"property:{string.Join(",", expandPropertyAliases)}" : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Api.Delivery.Rendering;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// Any tests contained within this class specifically test property expansion V2 (and field limiting) - not V1. If the
|
||||
/// aim is to test expansion for both versions, please put the tests in the base class.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
|
||||
{
|
||||
[TestCase("contentPicker", "contentPicker")]
|
||||
[TestCase("rootPicker", "nestedPicker")]
|
||||
public void OutputExpansionStrategy_CanExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor($"properties[{rootPropertyTypeAlias}[properties[{nestedPropertyTypeAlias}]]]");
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var nestedContentPickerContent = CreateSimplePickedContent(987, 654);
|
||||
var contentPickerContent = CreateMultiLevelPickedContent(123, nestedContentPickerContent, nestedPropertyTypeAlias, apiContentBuilder);
|
||||
var contentPickerContentProperty = CreateContentPickerProperty(content.Object, contentPickerContent.Key, rootPropertyTypeAlias, apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerContentProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties[rootPropertyTypeAlias] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerContent.Key, contentPickerOneOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOneOutput.Properties.Count);
|
||||
Assert.AreEqual(123, contentPickerOneOutput.Properties["number"]);
|
||||
|
||||
var nestedContentPickerOutput = contentPickerOneOutput.Properties[nestedPropertyTypeAlias] as ApiContent;
|
||||
Assert.IsNotNull(nestedContentPickerOutput);
|
||||
Assert.AreEqual(nestedContentPickerContent.Key, nestedContentPickerOutput.Id);
|
||||
Assert.IsNotEmpty(nestedContentPickerOutput.Properties);
|
||||
Assert.AreEqual(987, nestedContentPickerOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(654, nestedContentPickerOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandSpecifiedElement()
|
||||
{
|
||||
// var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "element" });
|
||||
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[$all]]]");
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
var apiElementBuilder = new ApiElementBuilder(accessor);
|
||||
|
||||
var contentPickerValue = CreateSimplePickedContent(111, 222);
|
||||
var contentPicker2Value = CreateSimplePickedContent(666, 777);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, 444, "number"),
|
||||
CreateElementProperty(content.Object, "element", 333, contentPickerValue.Key, "contentPicker", apiContentBuilder, apiElementBuilder),
|
||||
CreateElementProperty(content.Object, "element2", 555, contentPicker2Value.Key, "contentPicker", apiContentBuilder, apiElementBuilder));
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(3, result.Properties.Count);
|
||||
Assert.AreEqual(444, result.Properties["number"]);
|
||||
|
||||
var elementOutput = result.Properties["element"] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(333, elementOutput.Properties["number"]);
|
||||
var contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(contentPickerValue.Key, contentPickerOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOutput.Properties.Count);
|
||||
Assert.AreEqual(111, contentPickerOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(222, contentPickerOutput.Properties["numberTwo"]);
|
||||
|
||||
elementOutput = result.Properties["element2"] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(555, elementOutput.Properties["number"]);
|
||||
contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(contentPicker2Value.Key, contentPickerOutput.Id);
|
||||
Assert.AreEqual(0, contentPickerOutput.Properties.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandAllElements()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[$all]],element2[properties[$all]]]" );
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
var apiElementBuilder = new ApiElementBuilder(accessor);
|
||||
|
||||
var contentPickerValue = CreateSimplePickedContent(111, 222);
|
||||
var contentPicker2Value = CreateSimplePickedContent(666, 777);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(
|
||||
content,
|
||||
CreateNumberProperty(content.Object, 444, "number"),
|
||||
CreateElementProperty(content.Object, "element", 333, contentPickerValue.Key, "contentPicker", apiContentBuilder, apiElementBuilder),
|
||||
CreateElementProperty(content.Object, "element2", 555, contentPicker2Value.Key, "contentPicker", apiContentBuilder, apiElementBuilder));
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(3, result.Properties.Count);
|
||||
Assert.AreEqual(444, result.Properties["number"]);
|
||||
|
||||
var expectedElementOutputs = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
PropertyAlias = "element",
|
||||
ElementNumber = 333,
|
||||
ElementContentPicker = contentPickerValue.Key,
|
||||
ContentNumberOne = 111,
|
||||
ContentNumberTwo = 222
|
||||
},
|
||||
new
|
||||
{
|
||||
PropertyAlias = "element2",
|
||||
ElementNumber = 555,
|
||||
ElementContentPicker = contentPicker2Value.Key,
|
||||
ContentNumberOne = 666,
|
||||
ContentNumberTwo = 777
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var expectedElementOutput in expectedElementOutputs)
|
||||
{
|
||||
var elementOutput = result.Properties[expectedElementOutput.PropertyAlias] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(expectedElementOutput.ElementNumber, elementOutput.Properties["number"]);
|
||||
var contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(expectedElementOutput.ElementContentPicker, contentPickerOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOutput.Properties.Count);
|
||||
Assert.AreEqual(expectedElementOutput.ContentNumberOne, contentPickerOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(expectedElementOutput.ContentNumberTwo, contentPickerOutput.Properties["numberTwo"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_DoesNotExpandElementNestedContentPicker()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[contentPicker]]]" );
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
var apiElementBuilder = new ApiElementBuilder(accessor);
|
||||
|
||||
var nestedContentPickerValue = CreateSimplePickedContent(111, 222);
|
||||
var contentPickerValue = CreateMultiLevelPickedContent(987, nestedContentPickerValue, "contentPicker", apiContentBuilder);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(content, CreateElementProperty(content.Object, "element", 333, contentPickerValue.Key, "contentPicker", apiContentBuilder, apiElementBuilder));
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
|
||||
var elementOutput = result.Properties["element"] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(333, elementOutput.Properties["number"]);
|
||||
var contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(contentPickerValue.Key, contentPickerOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOutput.Properties.Count);
|
||||
Assert.AreEqual(987, contentPickerOutput.Properties["number"]);
|
||||
var nestedContentPickerOutput = contentPickerOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(nestedContentPickerOutput);
|
||||
Assert.AreEqual(nestedContentPickerValue.Key, nestedContentPickerOutput.Id);
|
||||
Assert.AreEqual(0, nestedContentPickerOutput.Properties.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandElementNestedContentPicker()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[contentPicker[properties[nestedContentPicker]]]]]");
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
var apiElementBuilder = new ApiElementBuilder(accessor);
|
||||
|
||||
var nestedContentPickerValue = CreateSimplePickedContent(111, 222);
|
||||
var contentPickerValue = CreateMultiLevelPickedContent(987, nestedContentPickerValue, "nestedContentPicker", apiContentBuilder);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
SetupContentMock(content, CreateElementProperty(content.Object, "element", 333, contentPickerValue.Key, "contentPicker", apiContentBuilder, apiElementBuilder));
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
|
||||
var elementOutput = result.Properties["element"] as IApiElement;
|
||||
Assert.IsNotNull(elementOutput);
|
||||
Assert.AreEqual(2, elementOutput.Properties.Count);
|
||||
Assert.AreEqual(333, elementOutput.Properties["number"]);
|
||||
var contentPickerOutput = elementOutput.Properties["contentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(contentPickerOutput);
|
||||
Assert.AreEqual(contentPickerValue.Key, contentPickerOutput.Id);
|
||||
Assert.AreEqual(2, contentPickerOutput.Properties.Count);
|
||||
Assert.AreEqual(987, contentPickerOutput.Properties["number"]);
|
||||
var nestedContentPickerOutput = contentPickerOutput.Properties["nestedContentPicker"] as IApiContent;
|
||||
Assert.IsNotNull(nestedContentPickerOutput);
|
||||
Assert.AreEqual(nestedContentPickerValue.Key, nestedContentPickerOutput.Id);
|
||||
Assert.AreEqual(2, nestedContentPickerOutput.Properties.Count);
|
||||
Assert.AreEqual(111, nestedContentPickerOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(222, nestedContentPickerOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutputExpansionStrategy_CanExpandContentPickerBeyondTwoLevels()
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor($"properties[level1Picker[properties[level2Picker[properties[level3Picker[properties[level4Picker]]]]]]]");
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var level5PickedContent = CreateSimplePickedContent(1234, 5678);
|
||||
var level4PickedContent = CreateMultiLevelPickedContent(444, level5PickedContent, "level4Picker", apiContentBuilder);
|
||||
var level3PickedContent = CreateMultiLevelPickedContent(333, level4PickedContent, "level3Picker", apiContentBuilder);
|
||||
var level2PickedContent = CreateMultiLevelPickedContent(222, level3PickedContent, "level2Picker", apiContentBuilder);
|
||||
var contentPickerContentProperty = CreateContentPickerProperty(content.Object, level2PickedContent.Key, "level1Picker", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerContentProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
|
||||
var level1PickerOutput = result.Properties["level1Picker"] as ApiContent;
|
||||
Assert.IsNotNull(level1PickerOutput);
|
||||
Assert.AreEqual(level2PickedContent.Key, level1PickerOutput.Id);
|
||||
Assert.AreEqual(2, level1PickerOutput.Properties.Count);
|
||||
Assert.AreEqual(222, level1PickerOutput.Properties["number"]);
|
||||
|
||||
var level2PickerOutput = level1PickerOutput.Properties["level2Picker"] as ApiContent;
|
||||
Assert.IsNotNull(level2PickerOutput);
|
||||
Assert.AreEqual(level3PickedContent.Key, level2PickerOutput.Id);
|
||||
Assert.AreEqual(2, level2PickerOutput.Properties.Count);
|
||||
Assert.AreEqual(333, level2PickerOutput.Properties["number"]);
|
||||
|
||||
var level3PickerOutput = level2PickerOutput.Properties["level3Picker"] as ApiContent;
|
||||
Assert.IsNotNull(level3PickerOutput);
|
||||
Assert.AreEqual(level4PickedContent.Key, level3PickerOutput.Id);
|
||||
Assert.AreEqual(2, level3PickerOutput.Properties.Count);
|
||||
Assert.AreEqual(444, level3PickerOutput.Properties["number"]);
|
||||
|
||||
var level4PickerOutput = level3PickerOutput.Properties["level4Picker"] as ApiContent;
|
||||
Assert.IsNotNull(level4PickerOutput);
|
||||
Assert.AreEqual(level5PickedContent.Key, level4PickerOutput.Id);
|
||||
Assert.AreEqual(2, level4PickerOutput.Properties.Count);
|
||||
Assert.AreEqual(1234, level4PickerOutput.Properties["numberOne"]);
|
||||
Assert.AreEqual(5678, level4PickerOutput.Properties["numberTwo"]);
|
||||
}
|
||||
|
||||
[TestCase("numberOne")]
|
||||
[TestCase("numberTwo")]
|
||||
public void OutputExpansionStrategy_CanLimitDirectFields(string includedField)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(fields: $"properties[{includedField}]");
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = CreateSimplePickedContent(123, 456);
|
||||
|
||||
var result = apiContentBuilder.Build(content);
|
||||
|
||||
Assert.AreEqual(1, result.Properties.Count);
|
||||
Assert.IsTrue(result.Properties.ContainsKey(includedField));
|
||||
Assert.AreEqual(includedField is "numberOne" ? 123 : 456, result.Properties[includedField]);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void OutputExpansionStrategy_CanLimitFieldsOfExpandedContent(bool expand)
|
||||
{
|
||||
var accessor = CreateOutputExpansionStrategyAccessor(expand ? "properties[$all]" : null, "properties[contentPickerOne[properties[numberOne]],contentPickerTwo[properties[numberTwo]]]");
|
||||
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
|
||||
|
||||
var content = new Mock<IPublishedContent>();
|
||||
|
||||
var contentPickerOneContent = CreateSimplePickedContent(12, 34);
|
||||
var contentPickerOneProperty = CreateContentPickerProperty(content.Object, contentPickerOneContent.Key, "contentPickerOne", apiContentBuilder);
|
||||
var contentPickerTwoContent = CreateSimplePickedContent(56, 78);
|
||||
var contentPickerTwoProperty = CreateContentPickerProperty(content.Object, contentPickerTwoContent.Key, "contentPickerTwo", apiContentBuilder);
|
||||
|
||||
SetupContentMock(content, contentPickerOneProperty, contentPickerTwoProperty);
|
||||
|
||||
var result = apiContentBuilder.Build(content.Object);
|
||||
|
||||
Assert.AreEqual(2, result.Properties.Count);
|
||||
|
||||
var contentPickerOneOutput = result.Properties["contentPickerOne"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerOneOutput);
|
||||
Assert.AreEqual(contentPickerOneContent.Key, contentPickerOneOutput.Id);
|
||||
// yeah we shouldn't test two things in one unit test, but given the risk of false positives when testing
|
||||
// conditional field limiting, this is preferable.
|
||||
if (expand)
|
||||
{
|
||||
Assert.AreEqual(1, contentPickerOneOutput.Properties.Count);
|
||||
Assert.AreEqual(12, contentPickerOneOutput.Properties["numberOne"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsEmpty(contentPickerOneOutput.Properties);
|
||||
}
|
||||
|
||||
var contentPickerTwoOutput = result.Properties["contentPickerTwo"] as ApiContent;
|
||||
Assert.IsNotNull(contentPickerTwoOutput);
|
||||
Assert.AreEqual(contentPickerTwoContent.Key, contentPickerTwoOutput.Id);
|
||||
if (expand)
|
||||
{
|
||||
Assert.AreEqual(1, contentPickerTwoOutput.Properties.Count);
|
||||
Assert.AreEqual(78, contentPickerTwoOutput.Properties["numberTwo"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsEmpty(contentPickerTwoOutput.Properties);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor(string? expand = null, string? fields = null)
|
||||
{
|
||||
var httpContextMock = new Mock<HttpContext>();
|
||||
var httpRequestMock = new Mock<HttpRequest>();
|
||||
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
|
||||
|
||||
httpRequestMock
|
||||
.SetupGet(r => r.Query)
|
||||
.Returns(new QueryCollection(new Dictionary<string, StringValues> { { "expand", expand }, { "fields", fields } }));
|
||||
|
||||
httpContextMock.SetupGet(c => c.Request).Returns(httpRequestMock.Object);
|
||||
httpContextAccessorMock.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object);
|
||||
IOutputExpansionStrategy outputExpansionStrategy = new RequestContextOutputExpansionStrategyV2(
|
||||
httpContextAccessorMock.Object,
|
||||
new ApiPropertyRenderer(new NoopPublishedValueFallback()),
|
||||
Mock.Of<ILogger<RequestContextOutputExpansionStrategyV2>>());
|
||||
var outputExpansionStrategyAccessorMock = new Mock<IOutputExpansionStrategyAccessor>();
|
||||
outputExpansionStrategyAccessorMock.Setup(s => s.TryGetValue(out outputExpansionStrategy)).Returns(true);
|
||||
|
||||
return outputExpansionStrategyAccessorMock.Object;
|
||||
}
|
||||
|
||||
protected override string? FormatExpandSyntax(bool expandAll = false, string[]? expandPropertyAliases = null)
|
||||
=> expandAll ? "$all" : expandPropertyAliases?.Any() is true ? $"properties[{string.Join(",", expandPropertyAliases)}]" : null;
|
||||
}
|
||||
Reference in New Issue
Block a user