Merge branch 'main' into v17/dev

# Conflicts:
#	src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
This commit is contained in:
Laura Neto
2025-07-22 15:39:26 +02:00
392 changed files with 4458 additions and 1242 deletions

View File

@@ -7,8 +7,8 @@
"name": "acceptancetest",
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.36",
"@umbraco/playwright-testhelpers": "^16.0.27",
"@umbraco/json-models-builders": "^2.0.37",
"@umbraco/playwright-testhelpers": "^16.0.28",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"
@@ -58,20 +58,19 @@
}
},
"node_modules/@umbraco/json-models-builders": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.36.tgz",
"integrity": "sha512-vQLL/y2ZIqrhCBe61W6YuA/C3KjGkva90WA09YfQuPL9HujOkJpVaP7KjJ0F8bBxwURy9tzMBFBLUz5fOdbkxQ==",
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.37.tgz",
"integrity": "sha512-97cRUrD+oeEno9I+qFjq7lWVS0+aDEK44lQQYWmVPAkCuAG1HMpBTEblS45CflrmLtgrDuZx68WIVVGpk9JzgQ==",
"dependencies": {
"camelize": "^1.0.1"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
"version": "16.0.27",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.27.tgz",
"integrity": "sha512-KxjIpfFsiK5b1Au8QrlWceK88eo53VxogLs0LMrxsRS3rt4rdmD1YRP6U+yIucdPKnhVgfIsh40J/taGAZyPFQ==",
"license": "MIT",
"version": "16.0.28",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.28.tgz",
"integrity": "sha512-l0RDfiXjQAtN2ykg7IX6ZSSW8GGLBm9RFO8WjsnRKAh7gZkH8+0DJdhx573m5bfZE6xa33TgyzIg9x+m/42/Lw==",
"dependencies": {
"@umbraco/json-models-builders": "2.0.36",
"@umbraco/json-models-builders": "2.0.37",
"node-fetch": "^2.6.7"
}
},

View File

@@ -20,8 +20,8 @@
"typescript": "^4.8.3"
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.36",
"@umbraco/playwright-testhelpers": "^16.0.27",
"@umbraco/json-models-builders": "^2.0.37",
"@umbraco/playwright-testhelpers": "^16.0.28",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"

View File

@@ -0,0 +1,200 @@
import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
// Content
const contentName = 'TestContentFromBlueprint';
const textContent = 'This is a block in document blueprint';
// Document Type
const documentTypeName = 'TestDocumentTypeForContent';
const groupName = 'TestGroup';
// Document Blueprint
const documentBlueprintName = 'TestDocumentBlueprint';
const documentBlueprintDanishName = 'TestDocumentBlueprint-DA';
// ElementType
const elementGroupName = 'ElementGroup';
const elementTypeName = 'TestElement';
const richTextDataTypeUiAlias = 'Richtext editor';
const elementDataTypeUiAlias = 'Umbraco.RichText';
const elementPropertyName = 'TipTapProperty'
// DataType
const blockDataTypeName = 'TestBlockEditor';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentBlueprint.ensureNameNotExists(documentBlueprintName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.document.ensureNameNotExists(documentBlueprintName);
await umbracoApi.documentBlueprint.ensureNameNotExists(documentBlueprintName);
await umbracoApi.documentType.ensureNameNotExists(elementTypeName);
await umbracoApi.dataType.ensureNameNotExists(blockDataTypeName);
});
test('can create content using an invariant document blueprint', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataTypeName = 'Textstring';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.documentBlueprint.createDocumentBlueprintWithTextBoxValue(documentBlueprintName, documentTypeId, dataTypeName, textContent);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(documentBlueprintName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(documentBlueprintName);
expect(contentData.values[0].value).toBe(textContent);
});
test('can create content using a variant document blueprint', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataTypeName = 'Textstring';
await umbracoApi.language.createDanishLanguage();
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.documentBlueprint.createDocumenBlueprintWithEnglishCultureAndDanishCultureAndTextBoxValue(documentBlueprintName, documentBlueprintDanishName, documentTypeId, dataTypeName, textContent);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(documentBlueprintName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(documentBlueprintName);
expect(contentData.values[0].value).toBe(textContent);
expect(contentData.variants[0].name).toBe(documentBlueprintName);
expect(contentData.variants[1].name).toBe(documentBlueprintDanishName);
// Clean
await umbracoApi.language.ensureNameNotExists('Danish');
});
test('can create content with different name using an invariant document blueprint', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataTypeName = 'Textstring';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.documentBlueprint.createDocumentBlueprintWithTextBoxValue(documentBlueprintName, documentTypeId, dataTypeName, textContent);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(documentBlueprintName)).toBeFalsy();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value).toBe(textContent);
});
test('can create content with different name using a variant document blueprint', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataTypeName = 'Textstring';
await umbracoApi.language.createDanishLanguage();
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.documentBlueprint.createDocumenBlueprintWithEnglishCultureAndDanishCultureAndTextBoxValue(documentBlueprintName, documentBlueprintDanishName, documentTypeId, dataTypeName, textContent);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButtonForContent();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value).toBe(textContent);
expect(contentData.variants[0].name).toBe(contentName);
expect(contentData.variants[1].name).toBe(documentBlueprintDanishName);
// Clean
await umbracoApi.language.ensureNameNotExists('Danish');
});
test('can create content using a document blueprint with block list', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(blockDataTypeName);
const tipTapDataType = await umbracoApi.dataType.getByName(richTextDataTypeUiAlias);
const tipTapDataTypeId = tipTapDataType.id;
const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, elementGroupName, elementPropertyName, tipTapDataTypeId);
await umbracoApi.documentBlueprint.createDefaultDocumentBlueprintWithABlockListEditorAndBlockWithValue(documentBlueprintName, documentTypeName, blockDataTypeName, elementTypeId, AliasHelper.toAlias(elementPropertyName), elementDataTypeUiAlias, textContent, groupName);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(documentBlueprintName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(documentBlueprintName);
expect(contentData.values[0].value.contentData[0].values[0].value.markup).toEqual(textContent);
const blockListValue = contentData.values.find(item => item.editorAlias === "Umbraco.BlockList")?.value;
expect(blockListValue).toBeTruthy();
});
test('can create content using a document blueprint with block grid', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(blockDataTypeName);
const tipTapDataType = await umbracoApi.dataType.getByName(richTextDataTypeUiAlias);
const tipTapDataTypeId = tipTapDataType.id;
const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, elementGroupName, elementPropertyName, tipTapDataTypeId);
await umbracoApi.documentBlueprint.createDefaultDocumentBlueprintWithABlockGridEditorAndBlockWithValue(documentBlueprintName, documentTypeName, blockDataTypeName, elementTypeId, AliasHelper.toAlias(elementPropertyName), richTextDataTypeUiAlias, textContent);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
await umbracoUi.content.waitForContentToBeCreated();
expect(await umbracoApi.document.doesNameExist(documentBlueprintName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(documentBlueprintName);
expect(contentData.values[0].value.contentData[0].values[0].value.markup).toEqual(textContent);
const blockListValue = contentData.values.find(item => item.editorAlias === "Umbraco.BlockGrid")?.value;
expect(blockListValue).toBeTruthy();
});

View File

@@ -240,27 +240,27 @@ test('can remove a icon color from a block', async ({umbracoApi, umbracoUi}) =>
expect(await umbracoApi.dataType.doesBlockEditorBlockContainIconColor(blockGridEditorName, contentElementTypeId, '')).toBeTruthy();
});
// TODO: Remove skip when the code is updated due to UI changes
test.skip('can add a thumbnail to a block', async ({umbracoApi, umbracoUi}) => {
test('can add a thumbnail to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const mediaName = 'TestMedia';
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const mediaId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const textStringData = await umbracoApi.dataType.getByName(dataTypeName);
const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id);
await umbracoApi.dataType.createBlockGridWithABlock(blockGridEditorName, contentElementTypeId);
const mediaUrl = await umbracoApi.media.getMediaPathByName(mediaName);
const mediaUrl = await umbracoApi.media.getMediaUrl(mediaId);
// Act
await umbracoUi.dataType.goToDataType(blockGridEditorName);
await umbracoUi.dataType.goToBlockWithName(elementTypeName);
await umbracoUi.dataType.goToBlockAdvancedTab();
await umbracoUi.dataType.chooseBlockThumbnailWithPath(mediaUrl.fileName, mediaUrl.mediaPath);
await umbracoUi.dataType.chooseBlockThumbnailWithPath(mediaUrl);
await umbracoUi.dataType.clickSubmitButton();
await umbracoUi.dataType.clickSaveButton();
// Assert
await umbracoUi.dataType.isSuccessStateVisibleForSaveButton();
await umbracoUi.dataType.doesBlockHaveThumbnailImage(mediaUrl);
});
// TODO: Remove skip when the code is updated. Currently it is missing the assertion steps

View File

@@ -414,9 +414,26 @@ test('can disable hide content editor in a block', async ({umbracoApi, umbracoUi
expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(false);
});
// TODO: Thumbnails are not showing in the UI
test.skip('can add a thumbnail to a block ', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
test('can add a thumbnail to a block', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const mediaName = 'TestMedia';
await umbracoApi.media.ensureNameNotExists(mediaName);
const mediaId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const textStringData = await umbracoApi.dataType.getByName(dataTypeName);
const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id);
await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId);
const mediaUrl = await umbracoApi.media.getMediaUrl(mediaId);
// Act
await umbracoUi.dataType.goToDataType(blockListEditorName);
await umbracoUi.dataType.goToBlockWithName(elementTypeName);
await umbracoUi.dataType.chooseBlockThumbnailWithPath(mediaUrl);
await umbracoUi.dataType.clickSubmitButton();
await umbracoUi.dataType.clickSaveButton();
// Assert
await umbracoUi.dataType.isSuccessStateVisibleForSaveButton();
await umbracoUi.dataType.doesBlockHaveThumbnailImage(mediaUrl);
});
// TODO: Thumbnails are not showing in the UI

View File

@@ -3,13 +3,10 @@ import {expect} from "@playwright/test";
const documentBlueprintName = 'TestDocumentBlueprints';
const documentTypeName = 'DocumentTypeForBlueprint';
let documentTypeId = '';
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentBlueprint.ensureNameNotExists(documentBlueprintName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
documentTypeId = await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
@@ -19,6 +16,8 @@ test.afterEach(async ({umbracoApi}) => {
test('can create a document blueprint from the settings menu', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
await umbracoUi.goToBackOffice();
await umbracoUi.documentBlueprint.goToSection(ConstantHelper.sections.settings);
// Act
@@ -29,17 +28,19 @@ test('can create a document blueprint from the settings menu', {tag: '@smoke'},
await umbracoUi.documentBlueprint.clickSaveButton();
// Assert
await umbracoUi.documentBlueprint.waitForDocumentBlueprintToBeCreated()
await umbracoUi.documentBlueprint.waitForDocumentBlueprintToBeCreated();
expect(await umbracoApi.documentBlueprint.doesNameExist(documentBlueprintName)).toBeTruthy();
await umbracoUi.documentBlueprint.isDocumentBlueprintRootTreeItemVisible(documentBlueprintName, true);
});
test('can rename a document blueprint', async ({umbracoApi, umbracoUi}) => {
// Arrange
const documentTypeId = await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
const wrongDocumentBlueprintName = 'Wrong Document Blueprint';
await umbracoApi.documentBlueprint.ensureNameNotExists(wrongDocumentBlueprintName);
await umbracoApi.documentBlueprint.createDefaultDocumentBlueprint(wrongDocumentBlueprintName, documentTypeId);
expect(await umbracoApi.documentBlueprint.doesNameExist(wrongDocumentBlueprintName)).toBeTruthy();
await umbracoUi.goToBackOffice();
await umbracoUi.documentBlueprint.goToSection(ConstantHelper.sections.settings);
// Act
@@ -57,8 +58,10 @@ test('can rename a document blueprint', async ({umbracoApi, umbracoUi}) => {
test('can delete a document blueprint', async ({umbracoApi, umbracoUi}) => {
// Arrange
const documentTypeId = await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
await umbracoApi.documentBlueprint.createDefaultDocumentBlueprint(documentBlueprintName, documentTypeId);
expect(await umbracoApi.documentBlueprint.doesNameExist(documentBlueprintName)).toBeTruthy();
await umbracoUi.goToBackOffice();
await umbracoUi.documentBlueprint.goToSection(ConstantHelper.sections.settings);
// Act
@@ -75,9 +78,9 @@ test('can delete a document blueprint', async ({umbracoApi, umbracoUi}) => {
test('can create a document blueprint from the content menu', async ({umbracoApi, umbracoUi}) => {
// Arrange
const documentTypeName = 'DocumentTypeForContent';
const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName);
await umbracoApi.document.createDefaultDocument(documentBlueprintName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
@@ -94,3 +97,29 @@ test('can create a document blueprint from the content menu', async ({umbracoApi
// Clean
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create a variant document blueprint', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.language.createDanishLanguage();
await umbracoApi.documentType.createDocumentTypeWithAllowVaryByCulture(documentTypeName);
await umbracoUi.goToBackOffice();
await umbracoUi.documentBlueprint.goToSection(ConstantHelper.sections.settings);
// Act
await umbracoUi.documentBlueprint.clickActionsMenuAtRoot();
await umbracoUi.documentBlueprint.clickCreateActionMenuOption();
await umbracoUi.documentBlueprint.clickTextButtonWithName(documentTypeName);
await umbracoUi.documentBlueprint.enterDocumentBlueprintName(documentBlueprintName);
await umbracoUi.documentBlueprint.clickSaveButton();
// Assert
await umbracoUi.documentBlueprint.waitForDocumentBlueprintToBeCreated();
expect(await umbracoApi.documentBlueprint.doesNameExist(documentBlueprintName)).toBeTruthy();
await umbracoUi.documentBlueprint.isDocumentBlueprintRootTreeItemVisible(documentBlueprintName, true);
await umbracoUi.documentBlueprint.page.on('console', message => {
expect(message.type()).not.toBe('error');
});
// Clean
await umbracoApi.language.ensureIsoCodeNotExists('da');
});

View File

@@ -7,7 +7,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.Builders;
internal class ContentDataBuilder : BuilderBase<ContentData>, IWithNameBuilder
internal sealed class ContentDataBuilder : BuilderBase<ContentData>, IWithNameBuilder
{
private Dictionary<string, CultureVariation> _cultureInfos;
private string _name;

View File

@@ -178,10 +178,9 @@ public class DataTypeBuilder
elementSettingKey,
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);
var dataTypeBuilder = new DataTypeBuilder()
.WithId(0)
.WithDatabaseType(ValueStorageType.Nvarchar)
.WithDatabaseType(ValueStorageType.Ntext)
.AddEditor()
.WithAlias(editorAlias);

View File

@@ -150,7 +150,7 @@ public static class UmbracoBuilderExtensions
}
// replace the default so there is no background index rebuilder
private class TestBackgroundIndexRebuilder : ExamineIndexRebuilder
private sealed class TestBackgroundIndexRebuilder : ExamineIndexRebuilder
{
public TestBackgroundIndexRebuilder(
IMainDom mainDom,

View File

@@ -60,6 +60,8 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
protected IShortStringHelper ShortStringHelper => Services.GetRequiredService<IShortStringHelper>();
protected IIdKeyMap IdKeyMap => Services.GetRequiredService<IIdKeyMap>();
protected GlobalSettings GlobalSettings => Services.GetRequiredService<IOptions<GlobalSettings>>().Value;
protected IMapperCollection Mappers => Services.GetRequiredService<IMapperCollection>();

View File

@@ -87,7 +87,6 @@ internal sealed class ContentTypeRepositoryTest : UmbracoIntegrationTest
AppCaches.Disabled,
LoggerFactory.CreateLogger<TemplateRepository>(),
FileSystems,
IOHelper,
ShortStringHelper,
Mock.Of<IViewHelper>(),
runtimeSettingsMock.Object);

View File

@@ -117,13 +117,13 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest
var runtimeSettingsMock = new Mock<IOptionsMonitor<RuntimeSettings>>();
runtimeSettingsMock.Setup(x => x.CurrentValue).Returns(new RuntimeSettings());
templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<TemplateRepository>(), FileSystems, IOHelper, ShortStringHelper, Mock.Of<IViewHelper>(), runtimeSettingsMock.Object);
templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<TemplateRepository>(), FileSystems, ShortStringHelper, Mock.Of<IViewHelper>(), runtimeSettingsMock.Object);
var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<TagRepository>());
var commonRepository =
new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper);
var languageRepository =
new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<LanguageRepository>());
contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger<RelationRepository>(), relationTypeRepository, entityRepository);

View File

@@ -0,0 +1,228 @@
using System.Data.Common;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class LongRunningOperationRepositoryTests : UmbracoIntegrationTest
{
[Test]
public async Task Get_ReturnsNull_WhenOperationDoesNotExist()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var result = await repository.GetAsync(Guid.NewGuid());
Assert.IsNull(result);
}
[Test]
public async Task Get_ReturnsExpectedOperation_WhenOperationExists()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var testOperation = _operations[1];
var result = await repository.GetAsync(testOperation.Operation.Id);
Assert.IsNotNull(result);
Assert.AreEqual(testOperation.Operation.Id, result.Id);
Assert.AreEqual(testOperation.Operation.Type, result.Type);
Assert.AreEqual(testOperation.Operation.Status, result.Status);
}
[TestCase("Test", new LongRunningOperationStatus[] { }, 0, 100, 5, 5)]
[TestCase("Test", new[] { LongRunningOperationStatus.Enqueued }, 0, 100, 1, 1)]
[TestCase("Test", new[] { LongRunningOperationStatus.Running }, 0, 100, 1, 1)]
[TestCase("Test", new[] { LongRunningOperationStatus.Enqueued, LongRunningOperationStatus.Running }, 0, 100, 2, 2)]
[TestCase("Test", new[] { LongRunningOperationStatus.Stale }, 0, 100, 1, 1)]
[TestCase("Test", new[] { LongRunningOperationStatus.Running, LongRunningOperationStatus.Stale }, 0, 100, 2, 2)]
[TestCase("Test", new[] { LongRunningOperationStatus.Success, LongRunningOperationStatus.Stale }, 0, 100, 2, 2)]
[TestCase("AnotherTest", new LongRunningOperationStatus[] { }, 0, 100, 1, 1)]
[TestCase("Test", new LongRunningOperationStatus[] { }, 0, 0, 0, 5)]
[TestCase("Test", new LongRunningOperationStatus[] { }, 0, 1, 1, 5)]
[TestCase("Test", new LongRunningOperationStatus[] { }, 2, 2, 2, 5)]
[TestCase("Test", new LongRunningOperationStatus[] { }, 5, 1, 0, 5)]
public async Task GetByType_ReturnsExpectedOperations(string type, LongRunningOperationStatus[] statuses, int skip, int take, int expectedCount, int expectedTotal)
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var result = await repository.GetByTypeAsync(type, statuses, skip, take);
Assert.IsNotNull(result);
Assert.AreEqual(expectedCount, result.Items.Count(), "Count of returned items should match the expected count");
Assert.AreEqual(expectedTotal, result.Total, "Total count should match the expected total count");
}
[Test]
public async Task GetStatus_ReturnsNull_WhenOperationDoesNotExist()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var result = await repository.GetStatusAsync(Guid.NewGuid());
Assert.IsNull(result);
}
[Test]
public async Task GetStatus_ReturnsExpectedStatus_WhenOperationExists()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var result = await repository.GetStatusAsync(_operations[0].Operation.Id);
Assert.AreEqual(_operations[0].Operation.Status, result);
}
[Test]
public async Task Create_InsertsOperationIntoDatabase()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var newOperation = new LongRunningOperation
{
Id = Guid.NewGuid(),
Type = "NewTest",
Status = LongRunningOperationStatus.Enqueued,
};
await repository.CreateAsync(newOperation, DateTimeOffset.UtcNow.AddMinutes(5));
var result = await repository.GetAsync(newOperation.Id);
Assert.IsNotNull(result);
Assert.AreEqual(newOperation.Id, result.Id);
Assert.AreEqual(newOperation.Type, result.Type);
Assert.AreEqual(newOperation.Status, result.Status);
}
[Test]
public async Task Create_ThrowsException_WhenOperationWithTheSameIdExists()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var newOperation = new LongRunningOperation
{
Id = _operations[0].Operation.Id,
Type = "NewTest",
Status = LongRunningOperationStatus.Enqueued,
};
Assert.ThrowsAsync(Is.InstanceOf<DbException>(), () => repository.CreateAsync(newOperation, DateTimeOffset.UtcNow.AddMinutes(5)));
}
[Test]
public async Task UpdateStatus_UpdatesOperationStatusInDatabase()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var testOperation = _operations[1];
repository.UpdateStatusAsync(testOperation.Operation.Id, LongRunningOperationStatus.Failed, DateTimeOffset.UtcNow);
var result = await repository.GetAsync(testOperation.Operation.Id);
Assert.IsNotNull(result);
Assert.AreEqual(LongRunningOperationStatus.Failed, result.Status);
}
[Test]
public async Task SetResult_UpdatesOperationResultInDatabase()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var testOperation = _operations[1];
var opResult = new LongRunningOperationResult { Result = true };
await repository.SetResultAsync(testOperation.Operation.Id, opResult);
var result = await repository.GetAsync<LongRunningOperationResult>(testOperation.Operation.Id);
Assert.IsNotNull(result);
Assert.IsNotNull(result.Result);
Assert.AreEqual(opResult.Result, result.Result.Result);
}
[Test]
public async Task CleanOperations_RemovesOldOperationsFromTheDatabase()
{
var provider = ScopeProvider;
using var scope = provider.CreateScope();
var repository = CreateRepository(provider);
await CreateTestData(repository);
var oldOperation = _operations[0];
// Check that the operation is present before cleaning
var result = await repository.GetAsync(oldOperation.Operation.Id);
Assert.IsNotNull(result);
await repository.CleanOperationsAsync(DateTimeOffset.UtcNow.AddMinutes(1));
// Check that the operation is removed after cleaning
result = await repository.GetAsync(oldOperation.Operation.Id);
Assert.IsNull(result);
}
private LongRunningOperationRepository CreateRepository(IScopeProvider provider)
=> new(GetRequiredService<IJsonSerializer>(), (IScopeAccessor)provider, AppCaches.Disabled, TimeProvider.System);
private async Task CreateTestData(LongRunningOperationRepository repository)
{
foreach (var op in _operations)
{
await repository.CreateAsync(op.Operation, op.ExpiresIn);
}
}
private readonly List<(LongRunningOperation Operation, DateTimeOffset ExpiresIn)> _operations =
[
(
Operation: new LongRunningOperation { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Success },
ExpiresIn: DateTimeOffset.UtcNow.AddMinutes(5)),
(
Operation: new LongRunningOperation { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Enqueued },
ExpiresIn: DateTimeOffset.UtcNow.AddMinutes(5)),
(
Operation: new LongRunningOperation { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Running },
ExpiresIn: DateTimeOffset.UtcNow.AddMinutes(5)),
(
Operation: new LongRunningOperation { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Running },
ExpiresIn: DateTimeOffset.UtcNow.AddMinutes(-1)),
(
Operation: new LongRunningOperation { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Failed },
ExpiresIn: DateTimeOffset.UtcNow.AddMinutes(-1)),
(
Operation: new LongRunningOperation { Id = Guid.NewGuid(), Type = "AnotherTest", Status = LongRunningOperationStatus.Success, },
ExpiresIn: DateTimeOffset.UtcNow.AddMinutes(5)),
];
private class LongRunningOperationResult
{
public bool Result { get; init; }
}
}

View File

@@ -56,7 +56,7 @@ internal sealed class MediaRepositoryTest : UmbracoIntegrationTest
new ContentTypeCommonRepository(scopeAccessor, TemplateRepository, appCaches, ShortStringHelper);
var languageRepository =
new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<LanguageRepository>());
mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<MediaTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<MediaTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<TagRepository>());
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);

View File

@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Builders;
@@ -411,7 +412,7 @@ internal sealed class MediaTypeRepositoryTest : UmbracoIntegrationTest
}
private MediaTypeRepository CreateRepository(IScopeProvider provider) =>
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<MediaTypeRepository>(), CommonRepository, LanguageRepository, ShortStringHelper);
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<MediaTypeRepository>(), CommonRepository, LanguageRepository, ShortStringHelper, IdKeyMap);
private EntityContainerRepository CreateContainerRepository(IScopeProvider provider) =>
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<EntityContainerRepository>(), Constants.ObjectTypes.MediaTypeContainer);

View File

@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Builders;
@@ -26,7 +27,7 @@ internal sealed class MemberTypeRepositoryTest : UmbracoIntegrationTest
{
var commonRepository = GetRequiredService<IContentTypeCommonRepository>();
var languageRepository = GetRequiredService<ILanguageRepository>();
return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of<ILogger<MemberTypeRepository>>(), commonRepository, languageRepository, ShortStringHelper);
return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of<ILogger<MemberTypeRepository>>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
}
[Test]

View File

@@ -63,7 +63,7 @@ internal sealed class TemplateRepositoryTest : UmbracoIntegrationTest
private IOptionsMonitor<RuntimeSettings> RuntimeSettings => GetRequiredService<IOptionsMonitor<RuntimeSettings>>();
private ITemplateRepository CreateRepository(IScopeProvider provider) =>
new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<TemplateRepository>(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, RuntimeSettings);
new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<TemplateRepository>(), FileSystems, ShortStringHelper, ViewHelper, RuntimeSettings);
[Test]
public void Can_Instantiate_Repository()
@@ -266,7 +266,7 @@ internal sealed class TemplateRepositoryTest : UmbracoIntegrationTest
var commonRepository =
new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches, ShortStringHelper);
var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<LanguageRepository>());
var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger<RelationRepository>(), relationTypeRepository, entityRepository);

View File

@@ -57,8 +57,7 @@ internal sealed class UserRepositoryTest : UmbracoIntegrationTest
Options.Create(new UserPasswordConfigurationSettings()),
new SystemTextJsonSerializer(),
mockRuntimeState.Object,
PermissionMappers,
AppPolicyCache);
PermissionMappers);
return repository;
}
@@ -165,8 +164,7 @@ internal sealed class UserRepositoryTest : UmbracoIntegrationTest
Options.Create(new UserPasswordConfigurationSettings()),
new SystemTextJsonSerializer(),
mockRuntimeState.Object,
PermissionMappers,
AppPolicyCache);
PermissionMappers);
repository2.Delete(user);

View File

@@ -1971,6 +1971,65 @@ internal sealed class ContentTypeServiceTests : UmbracoIntegrationTest
.Variations);
}
[Test]
public void Can_Create_Property_Type_Based_On_DataTypeKey()
{
// Arrange
var cts = ContentTypeService;
var dtdYesNo = DataTypeService.GetDataType(-49);
IContentType ctBase = new ContentType(ShortStringHelper, -1)
{
Name = "Base",
Alias = "Base",
Icon = "folder.gif",
Thumbnail = "folder.png"
};
ctBase.AddPropertyType(new PropertyType(ShortStringHelper, "ShouldNotMatter", ValueStorageType.Nvarchar)
{
Name = "Hide From Navigation",
Alias = Constants.Conventions.Content.NaviHide,
DataTypeKey = dtdYesNo.Key
});
cts.Save(ctBase);
// Assert
ctBase = cts.Get(ctBase.Key);
Assert.That(ctBase, Is.Not.Null);
Assert.That(ctBase.HasIdentity, Is.True);
Assert.That(ctBase.PropertyTypes.Count(), Is.EqualTo(1));
Assert.That(ctBase.PropertyTypes.First().DataTypeId, Is.EqualTo(dtdYesNo.Id));
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
}
[Test]
public void Can_Create_Property_Type_Based_On_PropertyEditorAlias()
{
// Arrange
var cts = ContentTypeService;
var dtdYesNo = DataTypeService.GetDataType(-49);
IContentType ctBase = new ContentType(ShortStringHelper, -1)
{
Name = "Base",
Alias = "Base",
Icon = "folder.gif",
Thumbnail = "folder.png"
};
ctBase.AddPropertyType(new PropertyType(ShortStringHelper, "Umbraco.TrueFalse", ValueStorageType.Nvarchar)
{
Name = "Hide From Navigation",
Alias = Constants.Conventions.Content.NaviHide,
});
cts.Save(ctBase);
// Assert
ctBase = cts.Get(ctBase.Key);
Assert.That(ctBase, Is.Not.Null);
Assert.That(ctBase.HasIdentity, Is.True);
Assert.That(ctBase.PropertyTypes.Count(), Is.EqualTo(1));
Assert.That(ctBase.PropertyTypes.First().DataTypeId, Is.EqualTo(dtdYesNo.Id));
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
}
private ContentType CreateComponent()
{
var component = new ContentType(ShortStringHelper, -1)

View File

@@ -18,7 +18,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.Importin
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class ImportResources {
internal sealed class ImportResources {
private static global::System.Resources.ResourceManager resourceMan;

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Tests.Integration.Umbraco.Persistence.EFCore.DbContext;
internal class UmbracoLock
internal sealed class UmbracoLock
{
public int Id { get; set; }

View File

@@ -2,7 +2,7 @@ using AutoFixture;
namespace Umbraco.Cms.Tests.UnitTests.AutoFixture.Customizations;
internal class OmitRecursionCustomization : ICustomization
internal sealed class OmitRecursionCustomization : ICustomization
{
public void Customize(IFixture fixture) =>
fixture.Behaviors.Add(new OmitOnRecursionBehavior());

View File

@@ -1,16 +1,13 @@
using System.Linq;
using AutoFixture;
using AutoFixture.Kernel;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Moq;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -20,7 +17,7 @@ using Umbraco.Cms.Web.Common.Security;
namespace Umbraco.Cms.Tests.UnitTests.AutoFixture.Customizations;
internal class UmbracoCustomizations : ICustomization
internal sealed class UmbracoCustomizations : ICustomization
{
public void Customize(IFixture fixture)
{

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Models;
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.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Tests.Common;
@@ -143,4 +144,75 @@ public class ContentBuilderTests : DeliveryApiTests
Assert.Null(result);
}
[TestCase("Shared value", "Segmented value", false)]
[TestCase(null, "Segmented value", false)]
[TestCase("Shared value", null, true)]
[TestCase(null, null, true)]
public void ContentBuilder_ReturnsNullForRequestedSegmentThatIsNotCreated(object? sharedValue, object? segmentedValue, bool expectNull)
{
var content = new Mock<IPublishedContent>();
var sharedPropertyValueConverter = new Mock<IDeliveryApiPropertyValueConverter>();
sharedPropertyValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(sharedValue is not null);
sharedPropertyValueConverter.Setup(p => p.ConvertIntermediateToDeliveryApiObject(
It.IsAny<IPublishedElement>(),
It.IsAny<IPublishedPropertyType>(),
It.IsAny<PropertyCacheLevel>(),
It.IsAny<object?>(),
It.IsAny<bool>(),
It.IsAny<bool>())).Returns(sharedValue);
sharedPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
sharedPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
sharedPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
var sharedPropertyType = SetupPublishedPropertyType(sharedPropertyValueConverter.Object, "sharedMessage", "Umbraco.Textstring");
var sharedProperty = new PublishedElementPropertyBase(sharedPropertyType, content.Object, false, PropertyCacheLevel.None, new VariationContext(), Mock.Of<ICacheManager>());
var segmentedPropertyValueConverter = new Mock<IDeliveryApiPropertyValueConverter>();
segmentedPropertyValueConverter.Setup(p => p.IsValue(It.IsAny<object?>(), It.IsAny<PropertyValueLevel>())).Returns(segmentedValue is not null);
segmentedPropertyValueConverter.Setup(p => p.ConvertIntermediateToDeliveryApiObject(
It.IsAny<IPublishedElement>(),
It.IsAny<IPublishedPropertyType>(),
It.IsAny<PropertyCacheLevel>(),
It.IsAny<object?>(),
It.IsAny<bool>(),
It.IsAny<bool>())).Returns(segmentedValue);
segmentedPropertyValueConverter.Setup(p => p.IsConverter(It.IsAny<IPublishedPropertyType>())).Returns(true);
segmentedPropertyValueConverter.Setup(p => p.GetPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
segmentedPropertyValueConverter.Setup(p => p.GetDeliveryApiPropertyCacheLevel(It.IsAny<IPublishedPropertyType>())).Returns(PropertyCacheLevel.None);
var segmentedPropertyType = SetupPublishedPropertyType(segmentedPropertyValueConverter.Object, "segmentedMessage", "Umbraco.Textstring", contentVariation: ContentVariation.Segment);
var segmentedProperty = new PublishedElementPropertyBase(segmentedPropertyType, content.Object, false, PropertyCacheLevel.None, new VariationContext(), Mock.Of<ICacheManager>());
var contentType = new Mock<IPublishedContentType>();
contentType.SetupGet(c => c.Alias).Returns("thePageType");
contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content);
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Segment);
var key = Guid.NewGuid();
var urlSegment = "url-segment";
var name = "The page";
ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, [sharedProperty, segmentedProperty]);
var routeBuilderMock = new Mock<IApiContentRouteBuilder>();
routeBuilderMock
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));
var variationContextAccessorMock = new Mock<IVariationContextAccessor>();
variationContextAccessorMock.Setup(v => v.VariationContext).Returns(new VariationContext(segment: "missingSegment"));
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilderMock.Object, CreateOutputExpansionStrategyAccessor(), variationContextAccessorMock.Object);
var result = builder.Build(content.Object);
if (expectNull)
{
Assert.IsNull(result);
}
else
{
Assert.IsNotNull(result);
}
}
}

View File

@@ -73,7 +73,12 @@ public class DeliveryApiTests
PublishStatusQueryService = publishStatusQueryService.Object;
}
protected IPublishedPropertyType SetupPublishedPropertyType(IPropertyValueConverter valueConverter, string propertyTypeAlias, string editorAlias, object? dataTypeConfiguration = null)
protected IPublishedPropertyType SetupPublishedPropertyType(
IPropertyValueConverter valueConverter,
string propertyTypeAlias,
string editorAlias,
object? dataTypeConfiguration = null,
ContentVariation contentVariation = ContentVariation.Nothing)
{
var mockPublishedContentTypeFactory = new Mock<IPublishedContentTypeFactory>();
mockPublishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny<int>()))
@@ -83,7 +88,7 @@ public class DeliveryApiTests
propertyTypeAlias,
123,
true,
ContentVariation.Nothing,
contentVariation,
new PropertyValueConverterCollection(() => new[] { valueConverter }),
Mock.Of<IPublishedModelFactory>(),
mockPublishedContentTypeFactory.Object);

View File

@@ -0,0 +1,35 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models;
[TestFixture]
public class DataTypeTests
{
[Test]
public void Can_Update_And_Verify_Dirty_Properties()
{
var dataType = new DataTypeBuilder()
.WithName("Test Data Type")
.WithDatabaseType(ValueStorageType.Ntext)
.Build();
dataType.ResetDirtyProperties();
dataType.DatabaseType = ValueStorageType.Nvarchar;
dataType.EditorUiAlias = "Test.EditorUiAlias";
var dirtyProperties = dataType.GetDirtyProperties().OrderBy(x => x).ToList();
Assert.IsTrue(dataType.IsPropertyDirty(nameof(dataType.DatabaseType)));
Assert.IsTrue(dataType.IsPropertyDirty(nameof(dataType.EditorUiAlias)));
Assert.AreEqual(2, dirtyProperties.Count);
Assert.AreEqual($"{nameof(dataType.DatabaseType)},{nameof(dataType.EditorUiAlias)}", string.Join(",", dirtyProperties));
}
}

View File

@@ -0,0 +1,417 @@
using System.Data;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
[TestFixture]
public class LongRunningOperationServiceTests
{
private ILongRunningOperationService _longRunningOperationService;
private Mock<ICoreScopeProvider> _scopeProviderMock;
private Mock<ILongRunningOperationRepository> _longRunningOperationRepositoryMock;
private Mock<TimeProvider> _timeProviderMock;
private Mock<ICoreScope> _scopeMock;
[SetUp]
public void Setup()
{
_scopeProviderMock = new Mock<ICoreScopeProvider>(MockBehavior.Strict);
_longRunningOperationRepositoryMock = new Mock<ILongRunningOperationRepository>(MockBehavior.Strict);
_timeProviderMock = new Mock<TimeProvider>(MockBehavior.Strict);
_scopeMock = new Mock<ICoreScope>();
_longRunningOperationService = new LongRunningOperationService(
Options.Create(new LongRunningOperationsSettings()),
_longRunningOperationRepositoryMock.Object,
_scopeProviderMock.Object,
_timeProviderMock.Object,
Mock.Of<ILogger<LongRunningOperationService>>());
}
[Test]
public async Task Run_ReturnsFailedAttempt_WhenOperationIsAlreadyRunning()
{
SetupScopeProviderMock();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetByTypeAsync("Test", It.IsAny<LongRunningOperationStatus[]>(), 0, 0))
.Callback<string, LongRunningOperationStatus[], int, int>((_, statuses, _, _) =>
{
Assert.AreEqual(2, statuses.Length);
Assert.Contains(LongRunningOperationStatus.Enqueued, statuses);
Assert.Contains(LongRunningOperationStatus.Running, statuses);
})
.ReturnsAsync(
new PagedModel<LongRunningOperation>
{
Total = 1,
Items = new List<LongRunningOperation> { new() { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Running } },
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.RunAsync(
"Test",
_ => Task.CompletedTask,
allowConcurrentExecution: false,
runInBackground: true);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsFalse(result.Success);
Assert.AreEqual(LongRunningOperationEnqueueStatus.AlreadyRunning, result.Status);
}
[Test]
public async Task Run_CreatesAndRunsOperation_WhenNotInBackground()
{
SetupScopeProviderMock();
_timeProviderMock.Setup(repo => repo.GetUtcNow())
.Returns(() => DateTime.UtcNow)
.Verifiable(Times.Exactly(2));
_longRunningOperationRepositoryMock
.Setup(repo => repo.CreateAsync(It.IsAny<LongRunningOperation>(), It.IsAny<DateTimeOffset>()))
.Callback<LongRunningOperation, DateTimeOffset>((op, exp) =>
{
Assert.AreEqual("Test", op.Type);
Assert.IsNotNull(op.Id);
Assert.AreEqual(LongRunningOperationStatus.Enqueued, op.Status);
})
.Returns(Task.CompletedTask)
.Verifiable(Times.Once);
_scopeProviderMock.Setup(scopeProvider => scopeProvider.Context)
.Returns(default(IScopeContext?))
.Verifiable(Times.Exactly(1));
var expectedStatuses = new List<LongRunningOperationStatus>
{
LongRunningOperationStatus.Enqueued,
LongRunningOperationStatus.Running,
LongRunningOperationStatus.Success,
};
_longRunningOperationRepositoryMock.Setup(repo => repo.UpdateStatusAsync(It.IsAny<Guid>(), It.IsAny<LongRunningOperationStatus>(), It.IsAny<DateTimeOffset>()))
.Callback<Guid, LongRunningOperationStatus, DateTimeOffset>((id, status, exp) =>
{
Assert.Contains(status, expectedStatuses);
})
.Returns(Task.CompletedTask);
var opCalls = 0;
var result = await _longRunningOperationService.RunAsync(
"Test",
_ =>
{
opCalls++;
return Task.CompletedTask;
},
allowConcurrentExecution: true,
runInBackground: false);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsTrue(result.Success);
Assert.AreEqual(LongRunningOperationEnqueueStatus.Success, result.Status);
Assert.AreEqual(1, opCalls, "Operation should have run and increased the call count, since it's not configured to run in the background.");
}
[Test]
public void Run_ThrowsException_WhenAttemptingToRunOperationNotInBackgroundInsideAScope()
{
SetupScopeProviderMock();
_scopeProviderMock.Setup(scopeProvider => scopeProvider.Context)
.Returns(new ScopeContext())
.Verifiable(Times.Exactly(1));
var opCalls = 0;
Assert.ThrowsAsync<InvalidOperationException>(async () => await _longRunningOperationService.RunAsync(
"Test",
_ =>
{
opCalls++;
return Task.CompletedTask;
},
allowConcurrentExecution: true,
runInBackground: false));
Assert.AreEqual(0, opCalls, "The operation should not have been called.");
}
[Test]
public async Task Run_CreatesAndQueuesOperation_WhenInBackground()
{
SetupScopeProviderMock();
_timeProviderMock.Setup(repo => repo.GetUtcNow())
.Returns(() => DateTime.UtcNow)
.Verifiable(Times.Exactly(2));
_longRunningOperationRepositoryMock
.Setup(repo => repo.CreateAsync(It.IsAny<LongRunningOperation>(), It.IsAny<DateTimeOffset>()))
.Callback<LongRunningOperation, DateTimeOffset>((op, exp) =>
{
Assert.AreEqual("Test", op.Type);
Assert.IsNotNull(op.Id);
Assert.AreEqual(LongRunningOperationStatus.Enqueued, op.Status);
})
.Returns(Task.CompletedTask)
.Verifiable(Times.Once);
var result = await _longRunningOperationService.RunAsync(
"Test",
_ => Task.CompletedTask,
allowConcurrentExecution: true,
runInBackground: true);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsTrue(result.Success);
Assert.AreEqual(LongRunningOperationEnqueueStatus.Success, result.Status);
}
[Test]
public async Task GetStatus_ReturnsExpectedStatus_WhenOperationExists()
{
SetupScopeProviderMock();
var operationId = Guid.NewGuid();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetStatusAsync(operationId))
.ReturnsAsync(LongRunningOperationStatus.Running)
.Verifiable(Times.Once);
var status = await _longRunningOperationService.GetStatusAsync(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsTrue(status.HasValue);
Assert.AreEqual(LongRunningOperationStatus.Running, status.Value);
}
[Test]
public async Task GetStatus_ReturnsNull_WhenOperationDoesNotExist()
{
SetupScopeProviderMock();
var operationId = Guid.NewGuid();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetStatusAsync(operationId))
.ReturnsAsync((LongRunningOperationStatus?)null)
.Verifiable(Times.Once);
var status = await _longRunningOperationService.GetStatusAsync(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsFalse(status.HasValue);
}
[Test]
public async Task GetByType_ReturnsExpectedOperations_WhenOperationsExist()
{
SetupScopeProviderMock();
const string operationType = "Test";
var operations = new List<LongRunningOperation>
{
new() { Id = Guid.NewGuid(), Type = operationType, Status = LongRunningOperationStatus.Running },
new() { Id = Guid.NewGuid(), Type = operationType, Status = LongRunningOperationStatus.Enqueued },
};
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetByTypeAsync(operationType, It.IsAny<LongRunningOperationStatus[]>(), 0, 100))
.Callback<string, LongRunningOperationStatus[], int, int>((_, statuses, _, _) =>
{
Assert.AreEqual(2, statuses.Length);
Assert.Contains(LongRunningOperationStatus.Enqueued, statuses);
Assert.Contains(LongRunningOperationStatus.Running, statuses);
})
.ReturnsAsync(
new PagedModel<LongRunningOperation>
{
Total = 2,
Items = operations,
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetByTypeAsync(operationType, 0, 100);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Items.Count());
Assert.AreEqual(2, result.Total);
Assert.IsTrue(result.Items.All(op => op.Type == operationType));
}
[Test]
public async Task GetByType_ReturnsExpectedOperations_WhenOperationsExistWithProvidedStatuses()
{
SetupScopeProviderMock();
const string operationType = "Test";
var operations = new List<LongRunningOperation>
{
new() { Id = Guid.NewGuid(), Type = operationType, Status = LongRunningOperationStatus.Failed },
};
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetByTypeAsync(operationType, It.IsAny<LongRunningOperationStatus[]>(), 0, 30))
.Callback<string, LongRunningOperationStatus[], int, int>((type, statuses, _, _) =>
{
Assert.AreEqual(1, statuses.Length);
Assert.Contains(LongRunningOperationStatus.Failed, statuses);
})
.ReturnsAsync(
new PagedModel<LongRunningOperation>
{
Total = 1,
Items = operations,
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetByTypeAsync(operationType, 0, 30, [LongRunningOperationStatus.Failed]);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Total);
Assert.AreEqual(1, result.Items.Count());
Assert.IsTrue(result.Items.All(op => op.Type == operationType));
}
[Test]
public async Task GetResult_ReturnsExpectedResult_WhenOperationExists()
{
SetupScopeProviderMock();
const string operationType = "Test";
var operationId = Guid.NewGuid();
const string expectedResult = "TestResult";
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetAsync<string>(operationId))
.ReturnsAsync(
new LongRunningOperation<string>
{
Id = operationId,
Type = operationType,
Status = LongRunningOperationStatus.Success,
Result = expectedResult,
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsTrue(result.Success);
Assert.AreEqual(LongRunningOperationResultStatus.Success, result.Status);
Assert.AreEqual(expectedResult, result.Result);
}
[Test]
public async Task GetResult_ReturnsFailedAttempt_WhenOperationDoesNotExist()
{
SetupScopeProviderMock();
var operationId = Guid.NewGuid();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetAsync<string>(operationId))
.ReturnsAsync(default(LongRunningOperation<string>))
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsFalse(result.Success);
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationNotFound);
Assert.IsNull(result.Result);
}
[Test]
public async Task GetResult_ReturnsFailedAttempt_WhenOperationFailed()
{
SetupScopeProviderMock();
const string operationType = "Test";
var operationId = Guid.NewGuid();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetAsync<string>(operationId))
.ReturnsAsync(
new LongRunningOperation<string>
{
Id = operationId,
Type = operationType,
Status = LongRunningOperationStatus.Failed,
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsFalse(result.Success);
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationFailed);
Assert.IsNull(result.Result);
}
[Test]
public async Task GetResult_ReturnsFailedAttempt_WhenOperationIsRunning()
{
SetupScopeProviderMock();
const string operationType = "Test";
var operationId = Guid.NewGuid();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetAsync<string>(operationId))
.ReturnsAsync(
new LongRunningOperation<string>
{
Id = operationId,
Type = operationType,
Status = LongRunningOperationStatus.Running,
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsFalse(result.Success);
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationPending);
Assert.IsNull(result.Result);
}
[Test]
public async Task GetResult_ReturnsFailedAttempt_WhenOperationIsEnqueued()
{
SetupScopeProviderMock();
const string operationType = "Test";
var operationId = Guid.NewGuid();
_longRunningOperationRepositoryMock
.Setup(repo => repo.GetAsync<string>(operationId))
.ReturnsAsync(
new LongRunningOperation<string>
{
Id = operationId,
Type = operationType,
Status = LongRunningOperationStatus.Enqueued,
})
.Verifiable(Times.Once);
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
_longRunningOperationRepositoryMock.VerifyAll();
Assert.IsFalse(result.Success);
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationPending);
Assert.IsNull(result.Result);
}
private void SetupScopeProviderMock() =>
_scopeProviderMock
.Setup(x => x.CreateCoreScope(
It.IsAny<IsolationLevel>(),
It.IsAny<RepositoryCacheMode>(),
It.IsAny<IEventDispatcher>(),
It.IsAny<IScopedNotificationPublisher>(),
It.IsAny<bool?>(),
It.IsAny<bool>(),
It.IsAny<bool>()))
.Returns(_scopeMock.Object);
}