Merge branch 'v16/dev' into v15/feature/select-segment

This commit is contained in:
Niels Lyngsø
2025-04-23 09:32:21 +02:00
committed by GitHub
21 changed files with 458 additions and 81 deletions

View File

@@ -53,7 +53,7 @@
<PackageVersion Include="MailKit" Version="4.10.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="2.5.192" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="NPoco" Version="5.7.1" />
@@ -100,4 +100,4 @@
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
</ItemGroup>
</Project>
</Project>

View File

@@ -13,7 +13,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[DeliveryApiAccess]
[VersionedDeliveryApiRoute("content")]
[ApiExplorerSettings(GroupName = "Content")]
[LocalizeFromAcceptLanguageHeader]
[ContextualizeFromAcceptHeaders]
[ValidateStartItem]
[AddVaryHeader]
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]

View File

@@ -49,6 +49,7 @@ public static class UmbracoBuilderExtensions
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
});
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();

View File

@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class ContextualizeFromAcceptHeadersAttribute : TypeFilterAttribute
{
public ContextualizeFromAcceptHeadersAttribute()
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
{
}
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
private readonly IRequestSegmmentService _requestSegmentService;
private readonly IVariationContextAccessor _variationContextAccessor;
public LocalizeFromAcceptLanguageHeaderAttributeFilter(
IRequestCultureService requestCultureService,
IRequestSegmmentService requestSegmentService,
IVariationContextAccessor variationContextAccessor)
{
_requestCultureService = requestCultureService;
_requestSegmentService = requestSegmentService;
_variationContextAccessor = variationContextAccessor;
}
public void OnActionExecuting(ActionExecutingContext context)
{
var requestedCulture = _requestCultureService.GetRequestedCulture().NullOrWhiteSpaceAsNull();
var requestedSegment = _requestSegmentService.GetRequestedSegment().NullOrWhiteSpaceAsNull();
if (requestedCulture.IsNullOrWhiteSpace() && requestedSegment.IsNullOrWhiteSpace())
{
return;
}
// contextualize the request
// NOTE: request culture or segment can be null here (but not both), so make sure to retain any existing
// context by means of fallback to current variation context (if available)
_variationContextAccessor.VariationContext = new VariationContext(
requestedCulture ?? _variationContextAccessor.VariationContext?.Culture,
requestedSegment ?? _variationContextAccessor.VariationContext?.Segment);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}

View File

@@ -1,37 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class LocalizeFromAcceptLanguageHeaderAttribute : TypeFilterAttribute
{
public LocalizeFromAcceptLanguageHeaderAttribute()
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
{
}
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
public LocalizeFromAcceptLanguageHeaderAttributeFilter(IRequestCultureService requestCultureService)
=> _requestCultureService = requestCultureService;
public void OnActionExecuting(ActionExecutingContext context)
{
var requestedCulture = _requestCultureService.GetRequestedCulture();
if (requestedCulture.IsNullOrWhiteSpace())
{
return;
}
_requestCultureService.SetRequestCulture(requestedCulture);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}

View File

@@ -33,6 +33,20 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
}
});
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Segment",
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{ "Segment One", new OpenApiExample { Value = new OpenApiString("segment-one") } }
}
});
AddApiKey(operation);
operation.Parameters.Add(new OpenApiParameter

View File

@@ -2,17 +2,15 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed partial class RequestCultureService : RequestHeaderHandler, IRequestCultureService
{
private readonly IVariationContextAccessor _variationContextAccessor;
public RequestCultureService(IHttpContextAccessor httpContextAccessor, IVariationContextAccessor variationContextAccessor)
: base(httpContextAccessor) =>
_variationContextAccessor = variationContextAccessor;
public RequestCultureService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
/// <inheritdoc />
public string? GetRequestedCulture()
@@ -21,15 +19,10 @@ internal sealed partial class RequestCultureService : RequestHeaderHandler, IReq
return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null;
}
/// <inheritdoc />
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
public void SetRequestCulture(string culture)
{
if (_variationContextAccessor.VariationContext?.Culture == culture)
{
return;
}
_variationContextAccessor.VariationContext = new VariationContext(culture);
// no-op
}
// at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8",

View File

@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
@@ -11,14 +10,19 @@ namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutingService
{
private readonly IRequestCultureService _requestCultureService;
private readonly IVariationContextAccessor _variationContextAccessor;
public RequestRoutingService(
IDomainCache domainCache,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) =>
IRequestCultureService requestCultureService,
IVariationContextAccessor variationContextAccessor)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_variationContextAccessor = variationContextAccessor;
}
/// <inheritdoc />
public string GetContentRoute(string requestedPath)
@@ -50,9 +54,14 @@ internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutin
}
// the Accept-Language header takes precedence over configured domain culture
if (domainAndUri.Culture != null && _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace())
if (domainAndUri.Culture != null
&& _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace()
&& _variationContextAccessor.VariationContext?.Culture != domainAndUri.Culture)
{
_requestCultureService.SetRequestCulture(domainAndUri.Culture);
// update the variation context to match the requested domain culture while retaining any contextualized segment
_variationContextAccessor.VariationContext = new VariationContext(
culture: domainAndUri.Culture,
segment: _variationContextAccessor.VariationContext?.Segment);
}
// when resolving content from a configured domain, the content cache expects the content route

View File

@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmmentService
{
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
/// <inheritdoc />
public string? GetRequestedSegment()
=> GetHeaderValue("Accept-Segment");
}

View File

@@ -7,9 +7,6 @@ public interface IRequestCultureService
/// </summary>
string? GetRequestedCulture();
/// <summary>
/// Updates the current request culture if applicable.
/// </summary>
/// <param name="culture">The culture to use for the current request.</param>
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
void SetRequestCulture(string culture);
}

View File

@@ -0,0 +1,9 @@
namespace Umbraco.Cms.Core.DeliveryApi;
public interface IRequestSegmmentService
{
/// <summary>
/// Gets the requested segment from the "Accept-Segment" header, if present.
/// </summary>
string? GetRequestedSegment();
}

View File

@@ -1,3 +1,5 @@
import { UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS } from '@umbraco-cms/backoffice/content-type';
const workspace: UmbExtensionManifest = {
type: 'workspaceView',
alias: 'Example.WorkspaceView.EntityContentTypeCondition',
@@ -10,7 +12,7 @@ const workspace: UmbExtensionManifest = {
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceContentTypeAlias',
alias: UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS,
//match : 'blogPost'
oneOf: ['blogPost', 'mediaType1'],
},

View File

@@ -0,0 +1,4 @@
/**
* Workspace Content Type Alias condition alias
*/
export const UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS = 'Umb.Condition.WorkspaceContentTypeAlias';

View File

@@ -1,20 +1,22 @@
import type { UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS } from './constants.js';
import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api';
export type UmbWorkspaceContentTypeAliasConditionConfig =
UmbConditionConfigBase<'Umb.Condition.WorkspaceContentTypeAlias'> & {
/**
* Define a content type alias in which workspace this extension should be available
* @example
* Depends on implementation, but i.e. "article", "image", "blockPage"
*/
match?: string;
/**
* Define one or more content type aliases in which workspace this extension should be available
* @example
* ["article", "image", "blockPage"]
*/
oneOf?: Array<string>;
};
export type UmbWorkspaceContentTypeAliasConditionConfig = UmbConditionConfigBase<
typeof UMB_WORKSPACE_CONTENT_TYPE_ALIAS_CONDITION_ALIAS
> & {
/**
* Define a content type alias in which workspace this extension should be available
* @example
* Depends on implementation, but i.e. "article", "image", "blockPage"
*/
match?: string;
/**
* Define one or more content type aliases in which workspace this extension should be available
* @example
* ["article", "image", "blockPage"]
*/
oneOf?: Array<string>;
};
/**
* @deprecated Use `UmbWorkspaceContentTypeAliasConditionConfig` instead. This will be removed in Umbraco 17.
*/

View File

@@ -1,2 +1,3 @@
export * from './modals/constants.js';
export * from './workspace/constants.js';
export * from './conditions/constants.js';

View File

@@ -2,6 +2,7 @@ import type { CompositionTypeModel } from '@umbraco-cms/backoffice/external/back
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
export type * from './composition/types.js';
export type * from './conditions/types.js';
export type UmbPropertyContainerTypes = 'Group' | 'Tab';

View File

@@ -2,7 +2,7 @@ import type { UmbDocumentTypeItemModel, UmbDocumentTypeTreeItemModel } from '../
import { UMB_DOCUMENT_TYPE_WORKSPACE_MODAL } from '../../constants.js';
import { UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN } from '../../paths.js';
import { UmbDocumentTypePickerInputContext } from './input-document-type.context.js';
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit';
import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -105,6 +105,10 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
public override get value(): string | undefined {
return this.selection.length > 0 ? this.selection.join(',') : undefined;
}
@property({ type: Boolean, attribute: 'readonly' })
readonly?: boolean;
@state()
private _items?: Array<UmbDocumentTypeItemModel>;
@@ -176,7 +180,7 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
}
#renderAddButton() {
if (this.max > 0 && this.selection.length >= this.max) return nothing;
if (this.readonly || (this.max > 0 && this.selection.length >= this.max)) return nothing;
return html`
<uui-button
id="btn-add"
@@ -206,7 +210,14 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
<uui-ref-node-document-type id=${item.unique} name=${this.localize.string(item.name)} href=${href}>
${this.#renderIcon(item)}
<uui-action-bar slot="actions">
<uui-button @click=${() => this.#removeItem(item)} label=${this.localize.term('general_remove')}></uui-button>
${when(
!this.readonly,
() => html`
<uui-button
label=${this.localize.term('general_remove')}
@click=${() => this.#removeItem(item)}></uui-button>
`,
)}
</uui-action-bar>
</uui-ref-node-document-type>
`;

View File

@@ -9,6 +9,7 @@ export const manifest: ManifestPropertyEditorUi = {
label: 'Document Type Picker',
icon: 'icon-document-dashed-line',
group: 'advanced',
supportsReadOnly: true,
settings: {
properties: [
{

View File

@@ -23,6 +23,9 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement
this.onlyElementTypes = config.getValueByAlias('onlyPickElementTypes') ?? false;
}
@property({ type: Boolean, attribute: 'readonly' })
readonly = false;
@state()
min = 0;
@@ -43,6 +46,7 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement
.min=${this.min}
.max=${this.max}
.value=${this.value}
.readonly=${this.readonly}
.elementTypesOnly=${this.onlyElementTypes ?? false}
@change=${this.#onChange}>
</umb-input-document-type>

View File

@@ -249,6 +249,22 @@ internal sealed class OpenApiContractTest : UmbracoTestServerTestBase
}
}
},
{
"name": "Accept-Segment",
"in": "header",
"description": "Defines the segment to return. Use this when querying segment variant content items.",
"schema": {
"type": "string"
},
"examples": {
"Default": {
"value": ""
},
"Segment One": {
"value": "segment-one"
}
}
},
{
"name": "Api-Key",
"in": "header",
@@ -388,6 +404,22 @@ internal sealed class OpenApiContractTest : UmbracoTestServerTestBase
}
}
},
{
"name": "Accept-Segment",
"in": "header",
"description": "Defines the segment to return. Use this when querying segment variant content items.",
"schema": {
"type": "string"
},
"examples": {
"Default": {
"value": ""
},
"Segment One": {
"value": "segment-one"
}
}
},
{
"name": "Api-Key",
"in": "header",
@@ -519,6 +551,22 @@ internal sealed class OpenApiContractTest : UmbracoTestServerTestBase
}
}
},
{
"name": "Accept-Segment",
"in": "header",
"description": "Defines the segment to return. Use this when querying segment variant content items.",
"schema": {
"type": "string"
},
"examples": {
"Default": {
"value": ""
},
"Segment One": {
"value": "segment-one"
}
}
},
{
"name": "Api-Key",
"in": "header",
@@ -653,6 +701,22 @@ internal sealed class OpenApiContractTest : UmbracoTestServerTestBase
}
}
},
{
"name": "Accept-Segment",
"in": "header",
"description": "Defines the segment to return. Use this when querying segment variant content items.",
"schema": {
"type": "string"
},
"examples": {
"Default": {
"value": ""
},
"Segment One": {
"value": "segment-one"
}
}
},
{
"name": "Api-Key",
"in": "header",

View File

@@ -0,0 +1,232 @@
using Microsoft.AspNetCore.Http;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Api.Delivery.Services;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Tests.Common;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Delivery.Services;
[TestFixture]
public class RequestRoutingServiceTests
{
[TestCase(null)]
[TestCase("")]
public void Empty_Path_Yields_Nothing(string? path)
{
var subject = new RequestRoutingService(
Mock.Of<IDomainCache>(),
Mock.Of<IHttpContextAccessor>(),
Mock.Of<IRequestStartItemProviderAccessor>(),
Mock.Of<IRequestCultureService>(),
Mock.Of<IVariationContextAccessor>());
var result = subject.GetContentRoute(path!);
Assert.IsEmpty(result);
}
[Test]
public void Explicit_Start_Item_Yields_Path_Prefixed_With_Start_Item_Id()
{
var startItem = new Mock<IPublishedContent>();
startItem.SetupGet(m => m.Id).Returns(1234);
var requestStartItemProviderMock = new Mock<IRequestStartItemProvider>();
requestStartItemProviderMock.Setup(m => m.GetStartItem()).Returns(startItem.Object);
var requestStartItemProvider = requestStartItemProviderMock.Object;
var requestStartItemProviderAccessorMock = new Mock<IRequestStartItemProviderAccessor>();
requestStartItemProviderAccessorMock.Setup(m => m.TryGetValue(out requestStartItemProvider)).Returns(true);
var subject = new RequestRoutingService(
Mock.Of<IDomainCache>(),
Mock.Of<IHttpContextAccessor>(),
requestStartItemProviderAccessorMock.Object,
Mock.Of<IRequestCultureService>(),
Mock.Of<IVariationContextAccessor>());
var result = subject.GetContentRoute("/some/where");
Assert.AreEqual("1234/some/where", result);
}
[TestCase("some/where")]
[TestCase("/some/where")]
public void Without_Domain_Binding_Yields_Path_Prefixed_With_Slash(string requestedPath)
{
var requestStartItemProviderMock = new Mock<IRequestStartItemProvider>();
var requestStartItemProvider = requestStartItemProviderMock.Object;
var requestStartItemProviderAccessorMock = new Mock<IRequestStartItemProviderAccessor>();
requestStartItemProviderAccessorMock.Setup(m => m.TryGetValue(out requestStartItemProvider)).Returns(true);
var httpContextAccessorMock = CreateHttpContextAccessorMock();
var subject = new RequestRoutingService(
Mock.Of<IDomainCache>(),
httpContextAccessorMock.Object,
requestStartItemProviderAccessorMock.Object,
Mock.Of<IRequestCultureService>(),
Mock.Of<IVariationContextAccessor>());
var result = subject.GetContentRoute(requestedPath);
Assert.AreEqual("/some/where", result);
}
[TestCase("some/where")]
[TestCase("/some/where")]
public void With_Domain_Binding_Yields_Path_Prefixed_With_Domain_Content_Id(string requestedPath)
{
var requestStartItemProviderMock = new Mock<IRequestStartItemProvider>();
var requestStartItemProvider = requestStartItemProviderMock.Object;
var requestStartItemProviderAccessorMock = new Mock<IRequestStartItemProviderAccessor>();
requestStartItemProviderAccessorMock.Setup(m => m.TryGetValue(out requestStartItemProvider)).Returns(true);
var httpContextAccessorMock = CreateHttpContextAccessorMock();
var domainCacheMock = GetDomainCacheMock(null);
var subject = new RequestRoutingService(
domainCacheMock.Object,
httpContextAccessorMock.Object,
requestStartItemProviderAccessorMock.Object,
Mock.Of<IRequestCultureService>(),
Mock.Of<IVariationContextAccessor>());
var result = subject.GetContentRoute(requestedPath);
Assert.AreEqual("1234/some/where", result);
}
[Test]
public void Domain_Binding_Culture_Sets_Variation_Context()
{
var requestStartItemProviderMock = new Mock<IRequestStartItemProvider>();
var requestStartItemProvider = requestStartItemProviderMock.Object;
var requestStartItemProviderAccessorMock = new Mock<IRequestStartItemProviderAccessor>();
requestStartItemProviderAccessorMock.Setup(m => m.TryGetValue(out requestStartItemProvider)).Returns(true);
var httpContextAccessorMock = CreateHttpContextAccessorMock();
var domainCacheMock = GetDomainCacheMock("da-DK");
var variationContextAccessor = new TestVariationContextAccessor();
var subject = new RequestRoutingService(
domainCacheMock.Object,
httpContextAccessorMock.Object,
requestStartItemProviderAccessorMock.Object,
Mock.Of<IRequestCultureService>(),
variationContextAccessor);
var result = subject.GetContentRoute("/some/where");
Assert.AreEqual("1234/some/where", result);
Assert.IsNotNull(variationContextAccessor.VariationContext);
Assert.AreEqual("da-DK", variationContextAccessor.VariationContext.Culture);
}
[Test]
public void Domain_Binding_Culture_Does_Not_Overwrite_Existing_Segment_Context()
{
var requestStartItemProviderMock = new Mock<IRequestStartItemProvider>();
var requestStartItemProvider = requestStartItemProviderMock.Object;
var requestStartItemProviderAccessorMock = new Mock<IRequestStartItemProviderAccessor>();
requestStartItemProviderAccessorMock.Setup(m => m.TryGetValue(out requestStartItemProvider)).Returns(true);
var httpContextAccessorMock = CreateHttpContextAccessorMock();
var domainCacheMock = GetDomainCacheMock("da-DK");
var variationContextAccessor = new TestVariationContextAccessor
{
VariationContext = new VariationContext("it-IT", "some-segment")
};
var subject = new RequestRoutingService(
domainCacheMock.Object,
httpContextAccessorMock.Object,
requestStartItemProviderAccessorMock.Object,
Mock.Of<IRequestCultureService>(),
variationContextAccessor);
var result = subject.GetContentRoute("/some/where");
Assert.AreEqual("1234/some/where", result);
Assert.IsNotNull(variationContextAccessor.VariationContext);
Assert.Multiple(() =>
{
Assert.AreEqual("da-DK", variationContextAccessor.VariationContext.Culture);
Assert.AreEqual("some-segment", variationContextAccessor.VariationContext.Segment);
});
}
[Test]
public void Explicit_Request_Culture_Overrides_Domain_Binding_Culture()
{
var requestStartItemProviderMock = new Mock<IRequestStartItemProvider>();
var requestStartItemProvider = requestStartItemProviderMock.Object;
var requestStartItemProviderAccessorMock = new Mock<IRequestStartItemProviderAccessor>();
requestStartItemProviderAccessorMock.Setup(m => m.TryGetValue(out requestStartItemProvider)).Returns(true);
var httpContextAccessorMock = CreateHttpContextAccessorMock();
var domainCacheMock = GetDomainCacheMock("da-DK");
const string expectedCulture = "it-IT";
const string expectedSegment = "some-segment";
var variationContextAccessor = new TestVariationContextAccessor
{
VariationContext = new VariationContext(expectedCulture, expectedSegment)
};
var requestCultureServiceMock = new Mock<IRequestCultureService>();
requestCultureServiceMock
.Setup(m => m.GetRequestedCulture())
.Returns(expectedCulture);
var subject = new RequestRoutingService(
domainCacheMock.Object,
httpContextAccessorMock.Object,
requestStartItemProviderAccessorMock.Object,
requestCultureServiceMock.Object,
variationContextAccessor);
var result = subject.GetContentRoute("/some/where");
Assert.AreEqual("1234/some/where", result);
Assert.IsNotNull(variationContextAccessor.VariationContext);
Assert.Multiple(() =>
{
Assert.AreEqual(expectedCulture, variationContextAccessor.VariationContext.Culture);
Assert.AreEqual(expectedSegment, variationContextAccessor.VariationContext.Segment);
});
}
private static Mock<IHttpContextAccessor> CreateHttpContextAccessorMock()
{
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
httpContextAccessorMock
.SetupGet(m => m.HttpContext)
.Returns(
new DefaultHttpContext
{
Request =
{
Scheme = "https",
Host = new HostString("some.host"),
Path = "/",
QueryString = new QueryString(string.Empty)
}
});
return httpContextAccessorMock;
}
private static Mock<IDomainCache> GetDomainCacheMock(string? culture)
{
var domainCacheMock = new Mock<IDomainCache>();
domainCacheMock
.Setup(m => m.GetAll(It.IsAny<bool>()))
.Returns([
new Domain(1, "some.host", 1234, culture, false, 1)
]);
return domainCacheMock;
}
}