diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs index 80a77740d0..aeb2cfe4a6 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependency.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -34,7 +34,7 @@ public class ArtifactDependency /// /// true if the dependency is included when building a dependency tree and gets deployed in the correct order; otherwise, false. /// - public bool Ordering { get; } + public bool Ordering { get; internal set; } /// /// Gets the dependency mode. @@ -42,7 +42,7 @@ public class ArtifactDependency /// /// The dependency mode. /// - public ArtifactDependencyMode Mode { get; } + public ArtifactDependencyMode Mode { get; internal set; } /// /// Gets or sets the checksum. diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs index 7f2b05eaad..6446af1ad5 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -21,25 +21,39 @@ public class ArtifactDependencyCollection : ICollection /// public void Add(ArtifactDependency item) { - if (item.Mode == ArtifactDependencyMode.Exist && - _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) && - existingItem.Mode == ArtifactDependencyMode.Match) + if (_dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem)) { - // Don't downgrade dependency mode from Match to Exist - return; - } + // Update existing item + if (existingItem.Mode is ArtifactDependencyMode.Exist) + { + // Allow updating dependency mode from Exist to Match + existingItem.Mode = item.Mode; + } - _dependencies[item.Udi] = item; + if (existingItem.Ordering is false) + { + // Allow updating non-ordering to ordering + existingItem.Ordering = item.Ordering; + } + + if (string.IsNullOrEmpty(item.Checksum) is false) + { + // Allow updating checksum if set + existingItem.Checksum = item.Checksum; + } + } + else + { + // Add new item + _dependencies[item.Udi] = item; + } } /// public void Clear() => _dependencies.Clear(); /// - public bool Contains(ArtifactDependency item) - => _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) && - // Check whether it has the same or higher dependency mode - (existingItem.Mode == item.Mode || existingItem.Mode == ArtifactDependencyMode.Match); + public bool Contains(ArtifactDependency item) => _dependencies.ContainsKey(item.Udi); /// public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex); diff --git a/src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs b/src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs new file mode 100644 index 0000000000..3a133bc253 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs @@ -0,0 +1,169 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.Migrations.PreMigration; + +// we use a composer here so its easier to clean this up when we no longer need it. +// same for additional classes in the same file, nice and self contained +// should only be used to migrate from v13 to v14 +// ⚠️ FIXME: PLEASE DELETE THIS IN V14! ⚠️ +public class DataTypeSplitDataCollectorComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + } +} + +public class DataTypeSplitDataCollector : INotificationHandler +{ + private readonly DataEditorCollection _dataEditors; + private readonly IManifestParser _manifestParser; + private readonly IDataTypeService _dataTypeService; + private readonly IKeyValueService _keyValueService; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly IUmbracoVersion _umbracoVersion; + + public DataTypeSplitDataCollector( + DataEditorCollection dataEditors, + IManifestParser manifestParser, + IDataTypeService dataTypeService, + IKeyValueService keyValueService, + IJsonSerializer jsonSerializer, + ILogger logger, + ICoreScopeProvider coreScopeProvider, + IRuntimeState runtimeState, + IServerRoleAccessor serverRoleAccessor, + IUmbracoVersion umbracoVersion) + { + _dataEditors = dataEditors; + _manifestParser = manifestParser; + _dataTypeService = dataTypeService; + _keyValueService = keyValueService; + _jsonSerializer = jsonSerializer; + _logger = logger; + _coreScopeProvider = coreScopeProvider; + _runtimeState = runtimeState; + _serverRoleAccessor = serverRoleAccessor; + _umbracoVersion = umbracoVersion; + } + + public void Handle(UmbracoApplicationStartedNotification notification) + { + // should only be used to collect data in v13 + if (_umbracoVersion.Version.Major is not 13) + { + return; + } + + // only run this if the application is actually running and not in an install/upgrade state + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + // do not run on load balanced subscribers + if (_serverRoleAccessor.CurrentServerRole == ServerRole.Subscriber) + { + return; + } + + var manifestEditorsData = _manifestParser.CombinedManifest.PropertyEditors + .Select(pe => new EditorAliasSplitData(pe.Alias){EditorUiAlias = pe.Alias, EditorAlias = EditorAliasFromValueEditorValueType(pe.GetValueEditor().ValueType)}) + .ToDictionary(data => data.OriginalEditorAlias); + + _logger.LogDebug("Found {count} custom PropertyEditor(s) configured trough manifest files",manifestEditorsData.Count); + + var fromCodeEditorsData = _dataEditors + .Where(de => + de.GetType().Assembly.GetName().FullName + .StartsWith("umbraco.core", StringComparison.InvariantCultureIgnoreCase) is false + && de.GetType().Assembly.GetName().FullName + .StartsWith("umbraco.infrastructure", StringComparison.InvariantCultureIgnoreCase) is false) + .Select(de => new EditorAliasSplitData(de.Alias) { EditorAlias = de.Alias }) + .ToDictionary(data => data.OriginalEditorAlias); + + _logger.LogDebug("Found {count} custom PropertyEditor(s) configured trough code",fromCodeEditorsData.Count); + + var combinedEditorsData = new Dictionary(manifestEditorsData); + foreach (KeyValuePair pair in fromCodeEditorsData) + { + combinedEditorsData.Add(pair.Key,pair.Value); + } + + if (combinedEditorsData.Any() == false) + { + _logger.LogDebug("No custom PropertyEditors found, skipping collection datatype migration data."); + return; + } + + using ICoreScope coreScope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + + IEnumerable dataTypes = _dataTypeService.GetAll(); + + DataTypeEditorAliasMigrationData[] migrationData = dataTypes + .Where(dt => combinedEditorsData.ContainsKey(dt.EditorAlias)) + .Select(dt => new DataTypeEditorAliasMigrationData + { + DataTypeId = dt.Id, + EditorAlias = combinedEditorsData[dt.EditorAlias].EditorAlias, + EditorUiAlias = combinedEditorsData[dt.EditorAlias].EditorUiAlias + }).ToArray(); + + _logger.LogDebug("Collected migration data for {count} DataType(s) that use custom PropertyEditors",migrationData.Length); + + _keyValueService.SetValue("migrateDataEditorSplitCollectionData",_jsonSerializer.Serialize(migrationData)); + } + + private class EditorAliasSplitData + { + public EditorAliasSplitData(string originalEditorAlias) + { + OriginalEditorAlias = originalEditorAlias; + } + + public string OriginalEditorAlias { get; init; } + public string? EditorUiAlias { get; init; } + public string? EditorAlias { get; init; } + } + + private class DataTypeEditorAliasMigrationData + { + public int DataTypeId { get; set; } + public string? EditorUiAlias { get; init; } + public string? EditorAlias { get; init; } + } + + private string EditorAliasFromValueEditorValueType(string valueType) + { + switch (valueType) + { + case ValueTypes.Date: return "Umbraco.Plain.DateTime"; + case ValueTypes.DateTime: return "Umbraco.Plain.DateTime"; + case ValueTypes.Decimal: return "Umbraco.Plain.Decimal"; + case ValueTypes.Integer: return "Umbraco.Plain.Integer"; + case ValueTypes.Bigint: return "Umbraco.Plain.Integer"; + case ValueTypes.Json: return "Umbraco.Plain.Json"; + case ValueTypes.Time: return "Umbraco.Plain.Time"; + case ValueTypes.String: return "Umbraco.Plain.String"; + case ValueTypes.Xml: return "Umbraco.Plain.String"; + default:return string.Empty; + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 84eda76fa5..f1b205b3aa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1229,8 +1229,11 @@ AND umbracoNode.id <> @id", /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true /// when it should be false /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values - /// stored for each - /// property, culture and current/published version. + /// stored for each property, culture and current/published version. + /// + /// Some of the sql statements in this function have a tendency to take a lot of parameters (nodeIds) + /// as the WhereIn Npoco method translates all the nodeIds being passed in as parameters when using the SqlClient provider. + /// this results in to many parameters (>2100) error => We need to batch the calls /// private void RenormalizeDocumentEditedFlags( IReadOnlyCollection propertyTypeIds, @@ -1386,16 +1389,19 @@ AND umbracoNode.id <> @id", // Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false foreach (IGrouping editValue in toUpdate.GroupBy(x => x.Edited)) { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + // update in batches to account for maximum parameter count + foreach (IEnumerable batchedValues in editValue.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, batchedValues.Select(x => x.Id))); + } } // Now bulk update the umbracoDocument table - // we need to do this in batches as the WhereIn Npoco method translates to all the nodeIds being passed in as parameters when using the SqlClient provider - // this results in to many parameters (>2100) being passed to the client when there are a lot of documents being normalized foreach (IGrouping> groupByValue in editedDocument.GroupBy(x => x.Value)) { - foreach (IEnumerable> batch in groupByValue.InGroupsOf(2000)) + // update in batches to account for maximum parameter count + foreach (IEnumerable> batch in groupByValue.InGroupsOf(Constants.Sql.MaxParameterCount)) { Database.Execute(Sql().Update(u => u.Set(x => x.Edited, groupByValue.Key)) .WhereIn(x => x.NodeId, batch.Select(x => x.Key))); diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 8d523ecd4f..c61c621398 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -778,6 +778,7 @@ public class ContentController : ContentControllerBase /// The content id to copy /// The name of the blueprint /// + [Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)] [HttpPost] public ActionResult CreateBlueprintFromContent( [FromQuery] int contentId, @@ -833,7 +834,9 @@ public class ContentController : ContentControllerBase /// /// Saves content /// - [ContentSaveValidation] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] + [FileUploadCleanupFilter] + [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch public async Task?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -1993,6 +1996,7 @@ public class ContentController : ContentControllerBase return Ok(); } + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [HttpDelete] [HttpPost] public IActionResult DeleteBlueprint(int id) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 86c2194a4c..e8151559ac 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -180,6 +180,13 @@ public static partial class UmbracoBuilderExtensions policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionCreateBlueprintFromId, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionCreateBlueprintFromContent.ActionLetter, "contentId")); + }); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 100d089451..f7be9d129a 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -20,9 +20,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute { - public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) => + public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) + : base(typeof(ContentSaveValidationFilter)) + { Order = -3000; // More important than ModelStateInvalidFilter.FilterOrder - + Arguments = new object[] { skipUserAccessValidation }; + } private sealed class ContentSaveValidationFilter : IAsyncActionFilter { @@ -32,6 +35,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; private readonly IPropertyValidationService _propertyValidationService; + private readonly bool _skipUserAccessValidation; public ContentSaveValidationFilter( @@ -40,7 +44,8 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizationService localizationService) + ILocalizationService localizationService, + bool skipUserAccessValidation) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -49,6 +54,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute _authorizationService = authorizationService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizationService = localizationService; + _skipUserAccessValidation = skipUserAccessValidation; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) @@ -88,7 +94,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute return; } - if (!await ValidateUserAccessAsync(model, context)) + if (_skipUserAccessValidation is false && await ValidateUserAccessAsync(model, context) is false) { return; } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 0441f3a733..1b15409813 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -23,6 +23,7 @@ public static class AuthorizationPolicies public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string ContentPermissionCreateBlueprintFromId = nameof(ContentPermissionCreateBlueprintFromId); public const string MediaPermissionByResource = nameof(MediaPermissionByResource); public const string MediaPermissionPathById = nameof(MediaPermissionPathById); diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 8e3e327bd6..854a3095d7 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -42,6 +42,7 @@ + diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs index 21a06c3643..0ca0b2f579 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs @@ -58,21 +58,64 @@ public class IndexInitializer _contentTypeService = contentTypeService; } + [Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")] public IndexInitializer( IShortStringHelper shortStringHelper, PropertyEditorCollection propertyEditors, MediaUrlGeneratorCollection mediaUrlGenerators, IScopeProvider scopeProvider, ILoggerFactory loggerFactory, - IOptions contentSettings, IContentTypeService contentTypeService) + IOptions contentSettings, + ILocalizationService localizationService) : this( - shortStringHelper, - propertyEditors, - mediaUrlGenerators, - scopeProvider, - loggerFactory, - contentSettings, - StaticServiceProvider.Instance.GetRequiredService(), contentTypeService) + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + localizationService, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + [Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")] + public IndexInitializer( + IShortStringHelper shortStringHelper, + PropertyEditorCollection propertyEditors, + MediaUrlGeneratorCollection mediaUrlGenerators, + IScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IOptions contentSettings, + IContentTypeService contentTypeService) + : this( + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + StaticServiceProvider.Instance.GetRequiredService(), contentTypeService) + { + } + + [Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")] + public IndexInitializer( + IShortStringHelper shortStringHelper, + PropertyEditorCollection propertyEditors, + MediaUrlGeneratorCollection mediaUrlGenerators, + IScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IOptions contentSettings) + : this( + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs index f029c42749..82aa79a27f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -49,6 +47,71 @@ public class ArtifactBaseTests string.Join(",", artifact.Dependencies.Select(x => x.Udi.ToString()))); } + [Test] + public void Dependencies_Correctly_Updates_Mode() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep Match + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(false, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual(null, dependency.Checksum); + } + + [Test] + public void Dependencies_Correctly_Updates_Ordering() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep ordering (regardless of mode) + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Exist), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(true, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual(null, dependency.Checksum); + } + + [Test] + public void Dependencies_Correctly_Updates_Checksum() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep checksum + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, "123"), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, string.Empty), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(true, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual("123", dependency.Checksum); + } + private class TestArtifact : ArtifactBase { public TestArtifact(GuidUdi udi, IEnumerable dependencies = null)