Merge branch 'main' into v17/dev

# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
This commit is contained in:
Andy Butland
2025-08-18 07:16:06 +01:00
188 changed files with 3654 additions and 3106 deletions

View File

@@ -1,7 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Threading;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Extensions;
@@ -13,18 +12,21 @@ public abstract class RuntimeAppCacheTests : AppCacheTests
internal abstract IAppPolicyCache AppPolicyCache { get; }
[Test]
[Explicit("Testing for timeouts cannot work on VSTS.")]
public void Can_Add_And_Expire_Struct_Strongly_Typed_With_Null()
{
var now = DateTime.Now;
AppPolicyCache.Insert("DateTimeTest", () => now, new TimeSpan(0, 0, 0, 0, 200));
Assert.AreEqual(now, AppCache.GetCacheItem<DateTime>("DateTimeTest"));
Assert.AreEqual(now, AppCache.GetCacheItem<DateTime?>("DateTimeTest"));
AppPolicyCache.Insert("DateTimeTest", () => now, new TimeSpan(0, 0, 0, 0, 20));
var cachedDateTime = AppCache.GetCacheItem<DateTime>("DateTimeTest");
var cachedDateTimeNullable = AppCache.GetCacheItem<DateTime?>("DateTimeTest");
Assert.AreEqual(now, cachedDateTime);
Assert.AreEqual(now, cachedDateTimeNullable);
Thread.Sleep(300); // sleep longer than the cache expiration
Thread.Sleep(30); // sleep longer than the cache expiration
Assert.AreEqual(default(DateTime), AppCache.GetCacheItem<DateTime>("DateTimeTest"));
Assert.AreEqual(null, AppCache.GetCacheItem<DateTime?>("DateTimeTest"));
cachedDateTime = AppCache.GetCacheItem<DateTime>("DateTimeTest");
cachedDateTimeNullable = AppCache.GetCacheItem<DateTime?>("DateTimeTest");
Assert.AreEqual(default(DateTime), cachedDateTime);
Assert.AreEqual(null, cachedDateTimeNullable);
}
[Test]

View File

@@ -28,8 +28,6 @@ public class SliderPropertyValueEditorTests
true,
new object(),
new List<string> { "some", "values" },
Guid.NewGuid(),
new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid())
};
[TestCaseSource(nameof(InvalidCaseData))]
@@ -39,6 +37,20 @@ public class SliderPropertyValueEditorTests
Assert.IsNull(fromEditor);
}
[Test]
public void Can_Handle_Invalid_Values_From_Editor_Guid()
{
var fromEditor = FromEditor(Guid.NewGuid());
Assert.IsNull(fromEditor);
}
[Test]
public void Can_Handle_Invalid_Values_From_Editor_Udi()
{
var fromEditor = FromEditor(new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()));
Assert.IsNull(fromEditor);
}
[TestCase("1", 1)]
[TestCase("0", 0)]
[TestCase("-1", -1)]

View File

@@ -0,0 +1,153 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Routing;
[TestFixture]
public class ContentFinderByUrlNewTests
{
private const int DomainContentId = 1233;
private const int ContentId = 1234;
private static readonly Guid _contentKey = Guid.NewGuid();
private const string ContentPath = "/test-page";
private const string DomainHost = "example.com";
[TestCase(ContentPath, true)]
[TestCase("/missing-page", false)]
public async Task Can_Find_Invariant_Content(string path, bool expectSuccess)
{
var mockContent = CreateMockPublishedContent();
var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor();
var mockDocumentUrlService = CreateMockDocumentUrlService();
var mockPublishedContentCache = CreateMockPublishedContentCache(mockContent);
var sut = CreateContentFinder(mockUmbracoContextAccessor, mockDocumentUrlService, mockPublishedContentCache);
var publishedRequestBuilder = CreatePublishedRequestBuilder(path);
var result = await sut.TryFindContent(publishedRequestBuilder);
Assert.AreEqual(expectSuccess, result);
if (expectSuccess)
{
Assert.IsNotNull(publishedRequestBuilder.PublishedContent);
}
else
{
Assert.IsNull(publishedRequestBuilder.PublishedContent);
}
}
[TestCase(ContentPath, true, false, true)]
[TestCase("/missing-page", true, false, false)]
[TestCase(ContentPath, true, true, true)]
[TestCase(ContentPath, false, true, false)]
public async Task Can_Find_Invariant_Content_With_Domain(string path, bool setDomain, bool useStrictDomainMatching, bool expectSuccess)
{
var mockContent = CreateMockPublishedContent();
var mockUmbracoContextAccessor = CreateMockUmbracoContextAccessor();
var mockDocumentUrlService = CreateMockDocumentUrlService();
var mockPublishedContentCache = CreateMockPublishedContentCache(mockContent);
var sut = CreateContentFinder(
mockUmbracoContextAccessor,
mockDocumentUrlService,
mockPublishedContentCache,
new WebRoutingSettings
{
UseStrictDomainMatching = useStrictDomainMatching
});
var publishedRequestBuilder = CreatePublishedRequestBuilder(path, withDomain: setDomain);
var result = await sut.TryFindContent(publishedRequestBuilder);
Assert.AreEqual(expectSuccess, result);
if (expectSuccess)
{
Assert.IsNotNull(publishedRequestBuilder.PublishedContent);
}
else
{
Assert.IsNull(publishedRequestBuilder.PublishedContent);
}
}
private static Mock<IPublishedContent> CreateMockPublishedContent()
{
var mockContent = new Mock<IPublishedContent>();
mockContent
.SetupGet(x => x.Id)
.Returns(ContentId);
mockContent
.SetupGet(x => x.ContentType.ItemType)
.Returns(PublishedItemType.Content);
return mockContent;
}
private static Mock<IUmbracoContextAccessor> CreateMockUmbracoContextAccessor()
{
var mockUmbracoContext = new Mock<IUmbracoContext>();
var mockUmbracoContextAccessor = new Mock<IUmbracoContextAccessor>();
var umbracoContext = mockUmbracoContext.Object;
mockUmbracoContextAccessor
.Setup(x => x.TryGetUmbracoContext(out umbracoContext))
.Returns(true);
return mockUmbracoContextAccessor;
}
private static Mock<IDocumentUrlService> CreateMockDocumentUrlService()
{
var mockDocumentUrlService = new Mock<IDocumentUrlService>();
mockDocumentUrlService
.Setup(x => x.GetDocumentKeyByRoute(It.Is<string>(y => y == ContentPath), It.IsAny<string?>(), It.IsAny<int?>(), It.IsAny<bool>()))
.Returns(_contentKey);
return mockDocumentUrlService;
}
private static Mock<IPublishedContentCache> CreateMockPublishedContentCache(Mock<IPublishedContent> mockContent)
{
var mockPublishedContentCache = new Mock<IPublishedContentCache>();
mockPublishedContentCache
.Setup(x => x.GetById(It.IsAny<bool>(), It.Is<Guid>(y => y == _contentKey)))
.Returns(mockContent.Object);
return mockPublishedContentCache;
}
private static ContentFinderByUrlNew CreateContentFinder(
Mock<IUmbracoContextAccessor> mockUmbracoContextAccessor,
Mock<IDocumentUrlService> mockDocumentUrlService,
Mock<IPublishedContentCache> mockPublishedContentCache,
WebRoutingSettings? webRoutingSettings = null)
=> new(
new NullLogger<ContentFinderByUrlNew>(),
mockUmbracoContextAccessor.Object,
mockDocumentUrlService.Object,
mockPublishedContentCache.Object,
Mock.Of<IOptionsMonitor<WebRoutingSettings>>(x => x.CurrentValue == (webRoutingSettings ?? new WebRoutingSettings())));
private static PublishedRequestBuilder CreatePublishedRequestBuilder(string path, bool withDomain = false)
{
var publishedRequestBuilder = new PublishedRequestBuilder(new Uri($"https://example.com{path}"), Mock.Of<IFileService>());
if (withDomain)
{
publishedRequestBuilder.SetDomain(new DomainAndUri(new Domain(1, $"https://{DomainHost}/", DomainContentId, "en-US", false, 0), new Uri($"https://{DomainHost}{path}")));
}
return publishedRequestBuilder;
}
}

View File

@@ -0,0 +1,174 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using NUnit.Framework;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.Repositories;
[TestFixture]
internal sealed class SimilarNodeNameTests
{
public void Name_Is_Suffixed()
{
SimilarNodeName[] names = { new SimilarNodeName { Id = 1, Name = "Zulu" } };
var res = SimilarNodeName.GetUniqueName(names, 0, "Zulu");
Assert.AreEqual("Zulu (1)", res);
}
[Test]
public void Suffixed_Name_Is_Incremented()
{
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Zulu"}, new SimilarNodeName {Id = 2, Name = "Kilo (1)"},
new SimilarNodeName {Id = 3, Name = "Kilo"}
};
var res = SimilarNodeName.GetUniqueName(names, 0, "Kilo (1)");
Assert.AreEqual("Kilo (2)", res);
}
[Test]
public void Lower_Number_Suffix_Is_Inserted()
{
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Golf"}, new SimilarNodeName {Id = 2, Name = "Golf (2)"}
};
var res = SimilarNodeName.GetUniqueName(names, 0, "Golf");
Assert.AreEqual("Golf (1)", res);
}
[Test]
[TestCase(0, "Alpha", "Alpha (3)")]
[TestCase(0, "alpha", "alpha (3)")]
public void Case_Is_Ignored(int nodeId, string nodeName, string expected)
{
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Alpha"}, new SimilarNodeName {Id = 2, Name = "Alpha (1)"},
new SimilarNodeName {Id = 3, Name = "Alpha (2)"}
};
var res = SimilarNodeName.GetUniqueName(names, nodeId, nodeName);
Assert.AreEqual(expected, res);
}
[Test]
public void Empty_List_Causes_Unchanged_Name()
{
var names = new SimilarNodeName[] { };
var res = SimilarNodeName.GetUniqueName(names, 0, "Charlie");
Assert.AreEqual("Charlie", res);
}
[Test]
[TestCase(0, "", " (1)")]
[TestCase(0, null, " (1)")]
public void Empty_Name_Is_Suffixed(int nodeId, string nodeName, string expected)
{
var names = new SimilarNodeName[] { };
var res = SimilarNodeName.GetUniqueName(names, nodeId, nodeName);
Assert.AreEqual(expected, res);
}
[Test]
public void Matching_NoedId_Causes_No_Change()
{
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Kilo (1)"}, new SimilarNodeName {Id = 2, Name = "Yankee"},
new SimilarNodeName {Id = 3, Name = "Kilo"}
};
var res = SimilarNodeName.GetUniqueName(names, 1, "Kilo (1)");
Assert.AreEqual("Kilo (1)", res);
}
[Test]
public void Extra_MultiSuffixed_Name_Is_Ignored()
{
// Sequesnce is: Test, Test (1), Test (2)
// Ignore: Test (1) (1)
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Alpha (2)"}, new SimilarNodeName {Id = 2, Name = "Test"},
new SimilarNodeName {Id = 3, Name = "Test (1)"}, new SimilarNodeName {Id = 4, Name = "Test (2)"},
new SimilarNodeName {Id = 5, Name = "Test (1) (1)"}
};
var res = SimilarNodeName.GetUniqueName(names, 0, "Test");
Assert.AreEqual("Test (3)", res);
}
[Test]
public void Matched_Name_Is_Suffixed()
{
SimilarNodeName[] names = { new SimilarNodeName { Id = 1, Name = "Test" } };
var res = SimilarNodeName.GetUniqueName(names, 0, "Test");
Assert.AreEqual("Test (1)", res);
}
[Test]
public void MultiSuffixed_Name_Is_Icremented()
{
// "Test (1)" is treated as the "original" version of the name.
// "Test (1) (1)" is the suffixed result of a copy, and therefore is incremented
// Hence this test result should be the same as Suffixed_Name_Is_Incremented
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Test (1)"}, new SimilarNodeName {Id = 2, Name = "Test (1) (1)"}
};
var res = SimilarNodeName.GetUniqueName(names, 0, "Test (1) (1)");
Assert.AreEqual("Test (1) (2)", res);
}
[Test]
public void Suffixed_Name_Causes_Secondary_Suffix()
{
SimilarNodeName[] names = { new SimilarNodeName { Id = 6, Name = "Alpha (1)" } };
var res = SimilarNodeName.GetUniqueName(names, 0, "Alpha (1)");
Assert.AreEqual("Alpha (1) (1)", res);
}
[TestCase("Test (0)", "Test (0) (1)")]
[TestCase("Test (-1)", "Test (-1) (1)")]
[TestCase("Test (1) (-1)", "Test (1) (-1) (1)")]
public void NonPositive_Suffix_Is_Ignored(string suffix, string expected)
{
SimilarNodeName[] names = { new SimilarNodeName { Id = 6, Name = suffix } };
var res = SimilarNodeName.GetUniqueName(names, 0, suffix);
Assert.AreEqual(expected, res);
}
[Test]
public void Handles_Many_Similar_Names()
{
SimilarNodeName[] names =
{
new SimilarNodeName {Id = 1, Name = "Alpha (2)"},
new SimilarNodeName {Id = 2, Name = "Test"},
new SimilarNodeName {Id = 3, Name = "Test (1)"},
new SimilarNodeName {Id = 4, Name = "Test (2)"},
new SimilarNodeName {Id = 22, Name = "Test (1) (1)"}
};
var uniqueName = SimilarNodeName.GetUniqueName(names, 0, "Test");
Assert.AreEqual("Test (3)", uniqueName);
}
}

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache;
[TestFixture]
public class DocumentBreadthFirstKeyProviderTests
{

View File

@@ -0,0 +1,186 @@
using Microsoft.Extensions.Caching.Hybrid;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Infrastructure.HybridCache.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache.Extensions;
/// <summary>
/// Provides tests to cover the <see cref="HybridCacheExtensions"/> class.
/// </summary>
/// <remarks>
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
/// </remarks>
[TestFixture]
public class HybridCacheExtensionsTests
{
private Mock<Microsoft.Extensions.Caching.Hybrid.HybridCache> _cacheMock;
[SetUp]
public void TestInitialize()
{
_cacheMock = new Mock<Microsoft.Extensions.Caching.Hybrid.HybridCache>();
}
[Test]
public async Task ExistsAsync_WhenKeyExists_ShouldReturnTrue()
{
// Arrange
string key = "test-key";
var expectedValue = "test-value";
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
null!,
It.IsAny<Func<object, CancellationToken, ValueTask<object>>>(),
It.IsAny<HybridCacheEntryOptions>(),
null,
CancellationToken.None))
.ReturnsAsync(expectedValue);
// Act
var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key);
// Assert
Assert.IsTrue(exists);
}
[Test]
public async Task ExistsAsync_WhenKeyDoesNotExist_ShouldReturnFalse()
{
// Arrange
string key = "test-key";
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
null!,
It.IsAny<Func<object, CancellationToken, ValueTask<object>>>(),
It.IsAny<HybridCacheEntryOptions>(),
null,
CancellationToken.None))
.Returns((
string key,
object? state,
Func<object, CancellationToken, ValueTask<object>> factory,
HybridCacheEntryOptions? options,
IEnumerable<string>? tags,
CancellationToken token) =>
{
return factory(state!, token);
});
// Act
var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key);
// Assert
Assert.IsFalse(exists);
}
[Test]
public async Task TryGetValueAsync_WhenKeyExists_ShouldReturnTrueAndValueAsString()
{
// Arrange
string key = "test-key";
var expectedValue = "test-value";
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
null!,
It.IsAny<Func<object, CancellationToken, ValueTask<string>>>(),
It.IsAny<HybridCacheEntryOptions>(),
null,
CancellationToken.None))
.ReturnsAsync(expectedValue);
// Act
var (exists, value) = await HybridCacheExtensions.TryGetValueAsync<string>(_cacheMock.Object, key);
// Assert
Assert.IsTrue(exists);
Assert.AreEqual(expectedValue, value);
}
[Test]
public async Task TryGetValueAsync_WhenKeyExists_ShouldReturnTrueAndValueAsInteger()
{
// Arrange
string key = "test-key";
var expectedValue = 5;
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
null!,
It.IsAny<Func<object, CancellationToken, ValueTask<int>>>(),
It.IsAny<HybridCacheEntryOptions>(),
null,
CancellationToken.None))
.ReturnsAsync(expectedValue);
// Act
var (exists, value) = await HybridCacheExtensions.TryGetValueAsync<int>(_cacheMock.Object, key);
// Assert
Assert.IsTrue(exists);
Assert.AreEqual(expectedValue, value);
}
[Test]
public async Task TryGetValueAsync_WhenKeyExistsButValueIsNull_ShouldReturnTrueAndNullValue()
{
// Arrange
string key = "test-key";
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
null!,
It.IsAny<Func<object, CancellationToken, ValueTask<object>>>(),
It.IsAny<HybridCacheEntryOptions>(),
null,
CancellationToken.None))
.ReturnsAsync(null!);
// Act
var (exists, value) = await HybridCacheExtensions.TryGetValueAsync<int?>(_cacheMock.Object, key);
// Assert
Assert.IsTrue(exists);
Assert.IsNull(value);
}
[Test]
public async Task TryGetValueAsync_WhenKeyDoesNotExist_ShouldReturnFalseAndNull()
{
// Arrange
string key = "test-key";
_cacheMock.Setup(cache => cache.GetOrCreateAsync(
key,
null,
It.IsAny<Func<object?, CancellationToken, ValueTask<string>>>(),
It.IsAny<HybridCacheEntryOptions>(),
null,
CancellationToken.None))
.Returns((
string key,
object? state,
Func<object?, CancellationToken, ValueTask<string>> factory,
HybridCacheEntryOptions? options,
IEnumerable<string>? tags,
CancellationToken token) =>
{
return factory(state, token);
});
// Act
var (exists, value) = await HybridCacheExtensions.TryGetValueAsync<string>(_cacheMock.Object, key);
// Assert
Assert.IsFalse(exists);
Assert.IsNull(value);
}
}