using Microsoft.AspNetCore.Http; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.PublishedContent; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class PublishedContentFallbackTests : UmbracoIntegrationTest { private IContentService ContentService => GetRequiredService(); private IContentTypeService ContentTypeService => GetRequiredService(); private IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService(); private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); private IVariationContextAccessor VariationContextAccessor => GetRequiredService(); private IPublishedValueFallback PublishedValueFallback => GetRequiredService(); private ContentCacheRefresher ContentCacheRefresher => GetRequiredService(); private IApiContentBuilder ApiContentBuilder => GetRequiredService(); private ILanguageService LanguageService => GetRequiredService(); protected override void CustomTestSetup(IUmbracoBuilder builder) => builder .AddUmbracoHybridCache() .AddDeliveryApi(); [SetUp] public void SetUpTest() { var httpContextAccessor = GetRequiredService(); httpContextAccessor.HttpContext = new DefaultHttpContext { Request = { Scheme = "https", Host = new HostString("localhost"), Path = "/", QueryString = new QueryString(string.Empty) }, RequestServices = Services }; } [TestCase("Invariant title", "Segmented title", "Segmented title")] [TestCase(null, "Segmented title", "Segmented title")] [TestCase("Invariant title", null, "Invariant title")] [TestCase(null, null, null)] public async Task Property_Value_Performs_Fallback_To_Default_Segment_For_Templated_Rendering(string? invariantTitle, string? segmentedTitle, string? expectedResult) { var publishedContent = await SetupSegmentedContentAsync(invariantTitle, segmentedTitle); // NOTE: the TextStringValueConverter.ConvertIntermediateToObject() explicitly converts a null source value to an empty string var segmentedResult = publishedContent.Value(PublishedValueFallback, "title", segment: "s1"); Assert.AreEqual(expectedResult ?? string.Empty, segmentedResult); var invariantResult = publishedContent.Value(PublishedValueFallback, "title", segment: string.Empty); Assert.AreEqual(invariantTitle ?? string.Empty, invariantResult); } [TestCase("Invariant title", "Segmented title", "Segmented title")] [TestCase(null, "Segmented title", "Segmented title")] [TestCase("Invariant title", null, "Invariant title")] [TestCase(null, null, null)] public async Task Property_Value_Performs_Fallback_To_Default_Segment_For_Delivery_Api_Output(string? invariantTitle, string? segmentedTitle, string? expectedResult) { UmbracoContextFactory.EnsureUmbracoContext(); var publishedContent = await SetupSegmentedContentAsync(invariantTitle, segmentedTitle); VariationContextAccessor.VariationContext = new VariationContext(culture: null, segment: "s1"); var apiContent = ApiContentBuilder.Build(publishedContent); Assert.IsNotNull(apiContent); Assert.IsTrue(apiContent.Properties.TryGetValue("title", out var segmentedValue)); Assert.AreEqual(expectedResult, segmentedValue); VariationContextAccessor.VariationContext = new VariationContext(culture: null, segment: null); apiContent = ApiContentBuilder.Build(publishedContent); Assert.IsNotNull(apiContent); Assert.IsTrue(apiContent.Properties.TryGetValue("title", out var invariantValue)); Assert.AreEqual(invariantTitle, invariantValue); } [TestCase("Danish title", true)] [TestCase("Danish title", false)] [TestCase(null, true)] [TestCase(null, false)] public async Task Property_Value_Can_Perform_Explicit_Language_Fallback(string? danishTitle, bool performFallbackToDefaultLanguage) { var danishLanguage = new Language("da-DK", "Danish") { FallbackIsoCode = "en-US" }; await LanguageService.CreateAsync(danishLanguage, Constants.Security.SuperUserKey); UmbracoContextFactory.EnsureUmbracoContext(); const string englishTitle = "English title"; var publishedContent = await SetupCultureVariantContentAsync(englishTitle, danishTitle); VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK", segment: null); var danishValue = publishedContent.Value(PublishedValueFallback, "title"); Assert.AreEqual(danishTitle ?? string.Empty, danishValue); var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage; var fallbackValue = publishedContent.Value(PublishedValueFallback, "title", fallback: fallback); Assert.AreEqual(danishTitle ?? englishTitle, fallbackValue); VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US", segment: null); var englishValue = publishedContent.Value(PublishedValueFallback, "title"); Assert.AreEqual(englishTitle, englishValue); } private async Task SetupSegmentedContentAsync(string? invariantTitle, string? segmentedTitle) { var contentType = new ContentTypeBuilder() .WithAlias("theContentType") .WithContentVariation(ContentVariation.Segment) .AddPropertyType() .WithAlias("title") .WithName("Title") .WithDataTypeId(Constants.DataTypes.Textbox) .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) .WithValueStorageType(ValueStorageType.Nvarchar) .WithVariations(ContentVariation.Segment) .Done() .WithAllowAsRoot(true) .Build(); await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); var content = new ContentBuilder() .WithContentType(contentType) .WithName("Content") .Build(); content.SetValue("title", invariantTitle); content.SetValue("title", segmentedTitle, segment: "s1"); ContentService.Save(content); ContentService.Publish(content, ["*"]); return GetPublishedContent(content.Key); } private async Task SetupCultureVariantContentAsync(string englishTitle, string? danishTitle) { var contentType = new ContentTypeBuilder() .WithAlias("theContentType") .WithContentVariation(ContentVariation.Culture) .AddPropertyType() .WithAlias("title") .WithName("Title") .WithDataTypeId(Constants.DataTypes.Textbox) .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) .WithValueStorageType(ValueStorageType.Nvarchar) .WithVariations(ContentVariation.Culture) .Done() .WithAllowAsRoot(true) .Build(); await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); var content = new ContentBuilder() .WithContentType(contentType) .WithCultureName("en-US", "EN") .WithCultureName("da-DK", "DA") .WithName("Content") .Build(); content.SetValue("title", englishTitle, culture: "en-US"); content.SetValue("title", danishTitle, culture: "da-DK"); ContentService.Save(content); ContentService.Publish(content, ["en-US", "da-DK"]); return GetPublishedContent(content.Key); } private IPublishedContent GetPublishedContent(Guid key) { ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]); UmbracoContextAccessor.Clear(); var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext; var publishedContent = umbracoContext.Content.GetById(key); Assert.IsNotNull(publishedContent); return publishedContent; } }