diff --git a/configuration.json b/configuration.json index 093433bd8..1189db046 100644 --- a/configuration.json +++ b/configuration.json @@ -5,7 +5,7 @@ "MigrationTools": { "Version": "16.0", "Endpoints": { - "Source2": { + "Source": { "EndpointType": "TfsTeamProjectEndpoint", "Collection": "https://dev.azure.com/nkdagility-preview/", "Project": "migrationSource1", @@ -25,7 +25,7 @@ "IterationPath": "Iteration" } }, - "Target2": { + "Target": { "EndpointType": "TfsTeamProjectEndpoint", "Collection": "https://dev.azure.com/nkdagility-preview/", "Project": "migrationTest5", @@ -147,8 +147,8 @@ "PauseAfterEachWorkItem": false, "AttachRevisionHistory": false, "GenerateMigrationComment": true, - "SourceName": "Source2", - "TargetName": "Target2", + "SourceName": "Source", + "TargetName": "Target", "WorkItemIDs": [ 12 ], "MaxGracefulFailures": 0, "SkipRevisionWithInvalidIterationPath": false, diff --git a/docs/Reference/Generated/MigrationTools.xml b/docs/Reference/Generated/MigrationTools.xml index 3d94762e5..5536d08e4 100644 --- a/docs/Reference/Generated/MigrationTools.xml +++ b/docs/Reference/Generated/MigrationTools.xml @@ -258,37 +258,37 @@ - => @"topic/better-validation-message" + => @"topic/validation-refactor" - => @"cfd2ecbc" + => @"a9e012b0" - => @"cfd2ecbc75ebef85edf99afce6ae914752457888" + => @"a9e012b05801421a57a109dc86b8827c02524855" - => @"2024-09-14T07:08:51+01:00" + => @"2024-09-15T23:57:44+01:00" - => @"2" + => @"6" - => @"v16.0.3-Preview.2-2-gcfd2ecbc" + => @"v16.0.3-Preview.3-6-ga9e012b0" - => @"v16.0.3-Preview.2" + => @"v16.0.3-Preview.3" @@ -318,17 +318,17 @@ - => @"5" + => @"9" - => @"Preview.2" + => @"Preview.3" - => @"-Preview.2" + => @"-Preview.3" diff --git a/src/MigrationTools.Clients.TfsObjectModel.Tests/Processors/TfsWorkItemMigrationProcessorTests.cs b/src/MigrationTools.Clients.TfsObjectModel.Tests/Processors/TfsWorkItemMigrationProcessorTests.cs index a7279755b..47eddf5bd 100644 --- a/src/MigrationTools.Clients.TfsObjectModel.Tests/Processors/TfsWorkItemMigrationProcessorTests.cs +++ b/src/MigrationTools.Clients.TfsObjectModel.Tests/Processors/TfsWorkItemMigrationProcessorTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace MigrationTools.Processors.Tests @@ -25,7 +26,10 @@ public void OptionsValidator_Valid() var validator = new TfsWorkItemMigrationProcessorOptionsValidator(); var x = new TfsWorkItemMigrationProcessorOptions(); x.WIQLQuery = "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @TeamProject"; - Assert.IsTrue(validator.Validate(null, x).Succeeded); + x.SourceName = "source"; + x.TargetName = "target"; + ValidateOptionsResult result = validator.Validate(null, x); + Assert.IsTrue(result.Succeeded); } } diff --git a/src/MigrationTools.Clients.TfsObjectModel.Tests/ServiceProviderHelper.cs b/src/MigrationTools.Clients.TfsObjectModel.Tests/ServiceProviderHelper.cs index dd5c2e1a7..8cdd285ed 100644 --- a/src/MigrationTools.Clients.TfsObjectModel.Tests/ServiceProviderHelper.cs +++ b/src/MigrationTools.Clients.TfsObjectModel.Tests/ServiceProviderHelper.cs @@ -58,7 +58,7 @@ private static TfsTeamSettingsEndpointOptions GetTfsTeamEndPointOptions(string p { Collection = new Uri("https://dev.azure.com/nkdagility-preview/"), Project = project, - Authentication = new TfsAuthenticationOptions() + Authentication = new Endpoints.Infrastructure.TfsAuthenticationOptions() { AuthenticationMode = AuthenticationMode.AccessToken, AccessToken = TestingConstants.AccessToken, @@ -82,7 +82,7 @@ private static TfsEndpointOptions GetTfsEndPointOptions(string project) { Collection = new Uri("https://dev.azure.com/nkdagility-preview/"), Project = project, - Authentication = new TfsAuthenticationOptions() + Authentication = new Endpoints.Infrastructure.TfsAuthenticationOptions() { AuthenticationMode = AuthenticationMode.AccessToken, AccessToken = TestingConstants.AccessToken, diff --git a/src/MigrationTools.Clients.TfsObjectModel/EndPoints/Infrastructure/TfsTeamProjectAuthentication.cs b/src/MigrationTools.Clients.TfsObjectModel/EndPoints/Infrastructure/TfsTeamProjectAuthentication.cs index a096c424d..a198afa8b 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/EndPoints/Infrastructure/TfsTeamProjectAuthentication.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/EndPoints/Infrastructure/TfsTeamProjectAuthentication.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using MigrationTools.Options; using MigrationTools.Options.Infrastructure; using Newtonsoft.Json; @@ -11,7 +12,7 @@ namespace MigrationTools.Endpoints.Infrastructure { - public class TfsAuthenticationOptions + public class TfsAuthenticationOptions : IValidateOptions { [JsonConverter(typeof(StringEnumConverter))] public AuthenticationMode AuthenticationMode { get; set; } @@ -20,5 +21,44 @@ public class TfsAuthenticationOptions [JsonConverter(typeof(DefaultOnlyConverter), "** removed as a secret ***")] public string AccessToken { get; set; } + + public ValidateOptionsResult Validate(string name, TfsAuthenticationOptions options) + { + var errors = new List(); + // Validate Authentication properties based on AuthenticationMode + switch (options.AuthenticationMode) + { + case AuthenticationMode.AccessToken: + if (string.IsNullOrWhiteSpace(options.AccessToken)) + { + errors.Add("The AccessToken must not be null or empty when AuthenticationMode is set to 'AccessToken'. You must provide a PAT to use 'AccessToken' as the authentication mode. You can set this through the config at 'MigrationTools:Endpoints:{name}:Authentication:AccessToken', or you can set an environment variable of 'MigrationTools__Endpoints__{name}__Authentication__AccessToken'. Check the docs on https://nkdagility.com/learn/azure-devops-migration-tools/Reference/Endpoints/TfsTeamProjectEndpoint/"); + } + break; + + case AuthenticationMode.Windows: + if (options.NetworkCredentials == null) + { + errors.Add("The NetworkCredentials must be provided when AuthenticationMode is set to 'Windows'."); + } else + { + ValidateOptionsResult result = options.NetworkCredentials.Validate(name, options.NetworkCredentials); + if (!result.Succeeded) + { + errors.AddRange(result.Failures); + } + } + break; + case AuthenticationMode.Prompt: + break; + default: + errors.Add($"The AuthenticationMode '{options.AuthenticationMode}' is not supported."); + break; + } + if (errors.Any()) + { + return ValidateOptionsResult.Fail(errors); + } + return ValidateOptionsResult.Success; + } } } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsEndpointOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsEndpointOptions.cs index 9fdf7620e..0f36e7dc0 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsEndpointOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsEndpointOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using Microsoft.Extensions.Options; using MigrationTools.Endpoints.Infrastructure; using Newtonsoft.Json; @@ -55,7 +56,7 @@ public ValidateOptionsResult Validate(string name, TfsEndpointOptions options) // Validate ReflectedWorkItemIdField - Must not be null or empty if (string.IsNullOrWhiteSpace(options.ReflectedWorkItemIdField)) { - errors.Add("The ReflectedWorkItemIdField property must not be null or empty."); + errors.Add("The ReflectedWorkItemIdField property must not be null or empty. Check the docs on https://nkdagility.com/learn/azure-devops-migration-tools/setup/reflectedworkitemid/"); } // Validate LanguageMaps - Must exist @@ -63,6 +64,14 @@ public ValidateOptionsResult Validate(string name, TfsEndpointOptions options) { errors.Add("The LanguageMaps property must exist."); } + else + { + ValidateOptionsResult lmr= options.LanguageMaps.Validate(name, options.LanguageMaps); + if (lmr != ValidateOptionsResult.Success) + { + errors.AddRange(lmr.Failures); + } + } // Validate Authentication - Must exist if (options.Authentication == null) @@ -71,34 +80,17 @@ public ValidateOptionsResult Validate(string name, TfsEndpointOptions options) } else { - // Validate Authentication properties based on AuthenticationMode - switch (options.Authentication.AuthenticationMode) + ValidateOptionsResult lmr = options.Authentication.Validate(name, options.Authentication); + if (lmr != ValidateOptionsResult.Success) { - case AuthenticationMode.AccessToken: - if (string.IsNullOrWhiteSpace(options.Authentication.AccessToken)) - { - errors.Add("The AccessToken must not be null or empty when AuthenticationMode is set to 'AccessToken'."); - } - break; - - case AuthenticationMode.Windows: - if (options.Authentication.NetworkCredentials == null) - { - errors.Add("The NetworkCredentials must be provided when AuthenticationMode is set to 'Windows'."); - } - break; - case AuthenticationMode.Prompt: - break; - default: - errors.Add($"The AuthenticationMode '{options.Authentication.AuthenticationMode}' is not supported."); - break; + errors.AddRange(lmr.Failures); } } // Return failure if there are errors, otherwise success - if (errors.Count > 0) + if (errors.Any()) { - return ValidateOptionsResult.Fail(string.Join(Environment.NewLine, errors)); + return ValidateOptionsResult.Fail(errors); } return ValidateOptionsResult.Success; diff --git a/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsLanguageMapOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsLanguageMapOptions.cs index da668c4d2..f30123453 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsLanguageMapOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Endpoints/TfsLanguageMapOptions.cs @@ -1,8 +1,31 @@ -namespace MigrationTools.Endpoints +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; + +namespace MigrationTools.Endpoints { - public class TfsLanguageMapOptions + public class TfsLanguageMapOptions : IValidateOptions { public string AreaPath { get; set; } public string IterationPath { get; set; } + + public ValidateOptionsResult Validate(string name, TfsLanguageMapOptions options) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(options.AreaPath)) + { + errors.Add("The AreaPath property must not be null or empty."); + } + if (string.IsNullOrWhiteSpace(options.IterationPath)) + { + errors.Add("The IterationPath property must not be null or empty."); + } + if (errors.Any()) + { + ValidateOptionsResult.Fail(errors); + } + return ValidateOptionsResult.Success; + } } } \ No newline at end of file diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/Infra/TfsProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/Infra/TfsProcessor.cs index 08ec52200..ef2cbd46b 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/Infra/TfsProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/Infra/TfsProcessor.cs @@ -8,6 +8,7 @@ using MigrationTools; using MigrationTools.Clients; using MigrationTools.Enrichers; +using MigrationTools.Exceptions; using MigrationTools.Tools; namespace MigrationTools.Processors.Infrastructure @@ -19,9 +20,31 @@ protected TfsProcessor(IOptions options, TfsCommonTools tfsCom } - new public TfsTeamProjectEndpoint Source => (TfsTeamProjectEndpoint)base.Source; + new public TfsTeamProjectEndpoint Source + { + get + { + var endpoint = base.Source as TfsTeamProjectEndpoint; + if (endpoint == null) + { + throw new ConfigurationValidationException(Options, ValidateOptionsResult.Fail($"The Endpoint '{Options.SourceName}' specified for `{this.GetType().Name}` is of the wrong type! {nameof(TfsTeamProjectEndpoint)} was expected.")); + } + return endpoint; + } + } - new public TfsTeamProjectEndpoint Target => (TfsTeamProjectEndpoint)base.Target; + new public TfsTeamProjectEndpoint Target + { + get + { + var endpoint = base.Target as TfsTeamProjectEndpoint; + if (endpoint == null) + { + throw new ConfigurationValidationException(Options, ValidateOptionsResult.Fail($"The Endpoint '{Options.TargetName}' specified for `{this.GetType().Name}` is of the wrong type! {nameof(TfsTeamProjectEndpoint)} was expected.")); + } + return endpoint; + } + } new public TfsCommonTools CommonTools => (TfsCommonTools)base.CommonTools; diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptions.cs index 1e98fb49b..970a97665 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptions.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Options; using System.Text.RegularExpressions; +using System.DirectoryServices.AccountManagement; namespace MigrationTools.Processors { @@ -101,58 +102,8 @@ public class TfsWorkItemMigrationProcessorOptions : ProcessorOptions, IWorkItemP /// public bool SkipRevisionWithInvalidAreaPath { get; set; } + } - public class TfsWorkItemMigrationProcessorOptionsValidator : IValidateOptions - { - - public TfsWorkItemMigrationProcessorOptionsValidator() - { - - } - - public ValidateOptionsResult Validate(string name, TfsWorkItemMigrationProcessorOptions options) - { - ValidateOptionsResult result = new ValidateOptionsResult(); - // Check if WIQLQuery is provided - if (string.IsNullOrWhiteSpace(options.WIQLQuery)) - { - return ValidateOptionsResult.Fail("The WIQLQuery must be provided."); - } - - // Validate the presence of required elements in the WIQL query - if (!ContainsTeamProjectCondition(options.WIQLQuery)) - { - return ValidateOptionsResult.Fail("The WIQLQuery must contain the condition '[System.TeamProject] = @TeamProject'."); - } - - if (!UsesWorkItemsTable(options.WIQLQuery)) - { - return ValidateOptionsResult.Fail("The WIQLQuery must use 'WorkItems' after the 'FROM' clause and not 'WorkItemLinks'."); - } - - return ValidateOptionsResult.Success; - } - - // Check if the WIQL query contains the '[System.TeamProject] = @TeamProject' condition - private bool ContainsTeamProjectCondition(string query) - { - // Regex to match '[System.TeamProject] = @TeamProject' - string teamProjectPattern = @"\[System\.TeamProject\]\s*=\s*@TeamProject"; - - return Regex.IsMatch(query, teamProjectPattern, RegexOptions.IgnoreCase); - } - - // Check if the WIQL query is using 'WorkItems' and not 'WorkItemLinks' - private bool UsesWorkItemsTable(string query) - { - // Regex to ensure that 'FROM WorkItems' exists and 'WorkItemLinks' does not follow the FROM clause - string fromWorkItemsPattern = @"FROM\s+WorkItems\b"; - string fromWorkItemLinksPattern = @"FROM\s+WorkItemLinks\b"; - - // Return true if "WorkItems" is used and "WorkItemLinks" is not - return Regex.IsMatch(query, fromWorkItemsPattern, RegexOptions.IgnoreCase) && - !Regex.IsMatch(query, fromWorkItemLinksPattern, RegexOptions.IgnoreCase); - } - } + } \ No newline at end of file diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptionsValidator.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptionsValidator.cs new file mode 100644 index 000000000..430f58247 --- /dev/null +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessorOptionsValidator.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace MigrationTools.Processors +{ + public class TfsWorkItemMigrationProcessorOptionsValidator : IValidateOptions + { + public ValidateOptionsResult Validate(string name, TfsWorkItemMigrationProcessorOptions options) + { + var errors = new List(); + ValidateOptionsResult baseResult = options.Validate(name, options); + if (baseResult != ValidateOptionsResult.Success) + { + errors.AddRange(baseResult.Failures); + } + // Check if WIQLQuery is provided + if (string.IsNullOrWhiteSpace(options.WIQLQuery)) + { + errors.Add($"The WIQLQuery on {name} must be provided."); + } + else + { + // Validate the presence of required elements in the WIQL query + if (!ContainsTeamProjectCondition(options.WIQLQuery)) + { + errors.Add("The WIQLQuery on {name} must contain the condition '[System.TeamProject] = @TeamProject'."); + } + + if (!UsesWorkItemsTable(options.WIQLQuery)) + { + errors.Add("The WIQLQuery on {name} must use 'WorkItems' after the 'FROM' clause and not 'WorkItemLinks'."); + } + } + if (errors.Count > 0) + { + return ValidateOptionsResult.Fail(errors); + } + + return ValidateOptionsResult.Success; + } + + private bool ContainsTeamProjectCondition(string query) + { + // Regex to match '[System.TeamProject] = @TeamProject' + string teamProjectPattern = @"\[System\.TeamProject\]\s*=\s*@TeamProject"; + + return Regex.IsMatch(query, teamProjectPattern, RegexOptions.IgnoreCase); + } + + // Check if the WIQL query is using 'WorkItems' and not 'WorkItemLinks' + private bool UsesWorkItemsTable(string query) + { + // Regex to ensure that 'FROM WorkItems' exists and 'WorkItemLinks' does not follow the FROM clause + string fromWorkItemsPattern = @"FROM\s+WorkItems\b"; + string fromWorkItemLinksPattern = @"FROM\s+WorkItemLinks\b"; + + // Return true if "WorkItems" is used and "WorkItemLinks" is not + return Regex.IsMatch(query, fromWorkItemsPattern, RegexOptions.IgnoreCase) && + !Regex.IsMatch(query, fromWorkItemLinksPattern, RegexOptions.IgnoreCase); + } + } +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/ServiceCollectionExtensions.cs b/src/MigrationTools.Clients.TfsObjectModel/ServiceCollectionExtensions.cs index e74189559..dd766e9e5 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/ServiceCollectionExtensions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/ServiceCollectionExtensions.cs @@ -35,7 +35,7 @@ public static void AddMigrationToolServicesForClientTfs_Tools(this IServiceColle public static void AddMigrationToolServicesForClientTfs_Processors(this IServiceCollection context) { context.AddSingleton(); - context.AddSingleton, TfsWorkItemMigrationProcessorOptionsValidator>(); + context.AddTransient, TfsWorkItemMigrationProcessorOptionsValidator>(); context.AddSingleton(); context.AddSingleton(); diff --git a/src/MigrationTools/Endpoints/Infrastructure/EndpointRegistrationExtensions.cs b/src/MigrationTools/Endpoints/Infrastructure/EndpointRegistrationExtensions.cs index e13caee4d..488d01c16 100644 --- a/src/MigrationTools/Endpoints/Infrastructure/EndpointRegistrationExtensions.cs +++ b/src/MigrationTools/Endpoints/Infrastructure/EndpointRegistrationExtensions.cs @@ -54,7 +54,7 @@ private static void AddEndPointSingleton(IServiceCollection services, IConfigura if (validatorType != null) { var validator = Activator.CreateInstance(validatorType); - var validationResult = InvokeValidator(validator, endpointOptionsInstance); + var validationResult = InvokeValidator(validator, endpointOptionsInstance, endpointName); if (validationResult.Failed) { //throw new OptionsValidationException(endpointName, endpointOptionsType, validationResult.Failures); @@ -120,10 +120,10 @@ private static Type GetValidatorTypeForOptions(Type optionsType) } - private static ValidateOptionsResult InvokeValidator(object validator, IEndpointOptions optionsInstance) + private static ValidateOptionsResult InvokeValidator(object validator, IEndpointOptions optionsInstance, string name) { var validateMethod = validator.GetType().GetMethod("Validate"); - var validationResult = validateMethod?.Invoke(validator, new object[] { null, optionsInstance }); + var validationResult = validateMethod?.Invoke(validator, new object[] { name, optionsInstance }); return (ValidateOptionsResult)validationResult; diff --git a/src/MigrationTools/Exceptions/ConfigurationValidationException.cs b/src/MigrationTools/Exceptions/ConfigurationValidationException.cs index 061b11a1f..f4f579881 100644 --- a/src/MigrationTools/Exceptions/ConfigurationValidationException.cs +++ b/src/MigrationTools/Exceptions/ConfigurationValidationException.cs @@ -10,20 +10,27 @@ namespace MigrationTools.Exceptions { public class ConfigurationValidationException : Exception { - public IConfigurationSection ConfigrationSection { get; } + public string ConfigrationSectionPath { get; } public IOptions OptionsInstance { get; } public ValidateOptionsResult ValidationResult { get; } public ConfigurationValidationException(IConfigurationSection configrationSection, IOptions optionsInstance, ValidateOptionsResult validationResult) { - ConfigrationSection = configrationSection; + ConfigrationSectionPath = configrationSection.Path; + OptionsInstance = optionsInstance; + ValidationResult = validationResult; + } + + public ConfigurationValidationException(IOptions optionsInstance, ValidateOptionsResult validationResult) + { + ConfigrationSectionPath = optionsInstance.ConfigurationMetadata.PathToInstance; OptionsInstance = optionsInstance; ValidationResult = validationResult; } public override string ToString() { - return $"The configuration entry at '{ConfigrationSection.Path}' did not pass validation!\n Please check the following failures:\n -{string.Join("\n-", ValidationResult.Failures)}"; + return $"The configuration entry at '{ConfigrationSectionPath}' did not pass validation!\n Please check the following failures:\n -{string.Join("\n-", ValidationResult.Failures)}"; } } diff --git a/src/MigrationTools/Options/NetworkCredentialsOptions.cs b/src/MigrationTools/Options/NetworkCredentialsOptions.cs index 8bd265a4e..a8b3da8de 100644 --- a/src/MigrationTools/Options/NetworkCredentialsOptions.cs +++ b/src/MigrationTools/Options/NetworkCredentialsOptions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; using MigrationTools.Options.Infrastructure; using Newtonsoft.Json; @@ -11,12 +14,34 @@ public class NetworkCredentialsOptions public NetworkCredentials Target { get; set; } } - public class NetworkCredentials + public class NetworkCredentials : IValidateOptions { public string Domain { get; set; } public string UserName { get; set; } [JsonConverter(typeof(DefaultOnlyConverter), "** removed as a secret ***")] public string Password { get; set; } + + public ValidateOptionsResult Validate(string name, NetworkCredentials options) + { + var errors = new List(); + if (string.IsNullOrWhiteSpace(options.Domain)) + { + errors.Add("The Domain must not be null or empty."); + } + if (string.IsNullOrWhiteSpace(options.UserName)) + { + errors.Add("The UserName must not be null or empty."); + } + if (string.IsNullOrWhiteSpace(options.Password)) + { + errors.Add("The Password must not be null or empty."); + } + if (errors.Any()) + { + return ValidateOptionsResult.Fail(errors); + } + return ValidateOptionsResult.Success; + } } } diff --git a/src/MigrationTools/Processors/Infrastructure/Processor.cs b/src/MigrationTools/Processors/Infrastructure/Processor.cs index 7237aa459..14bafe1e6 100644 --- a/src/MigrationTools/Processors/Infrastructure/Processor.cs +++ b/src/MigrationTools/Processors/Infrastructure/Processor.cs @@ -65,10 +65,10 @@ public IEndpoint GetEndpoint(string name) throw new ArgumentException("Endpoint name cannot be null or empty", nameof(name)); } // Assuming GetRequiredKeyedService throws an exception if the service is not found - IEndpoint endpoint = Services.GetRequiredKeyedService(name); + IEndpoint endpoint = Services.GetKeyedService(name); if (endpoint == null) { - Log.LogCritical("Processor::GetEndpoint: The endpoint '{EndpointName}' could not be found.", name); + throw new ConfigurationValidationException( Options, ValidateOptionsResult.Fail($"The Endpoint '{name}' specified for `{this.GetType().Name}` was not found.")); } return endpoint; } @@ -105,14 +105,12 @@ public void Execute() { Status = ProcessingStatus.Failed; ProcessorActivity.SetStatus(ActivityStatusCode.Error); - Telemetry.TrackException(ex, ProcessorActivity.Tags); Log.LogCritical(ex, "Validation of your configuration failed:"); } catch (ConfigurationValidationException ex) { Status = ProcessingStatus.Failed; ProcessorActivity.SetStatus(ActivityStatusCode.Error); - Telemetry.TrackException(ex, ProcessorActivity.Tags); Log.LogCritical(ex, "Validation of your configuration failed:"); } catch (Exception ex) diff --git a/src/MigrationTools/Processors/Infrastructure/ProcessorOptions.cs b/src/MigrationTools/Processors/Infrastructure/ProcessorOptions.cs index 8a8f9daa0..92ed9a478 100644 --- a/src/MigrationTools/Processors/Infrastructure/ProcessorOptions.cs +++ b/src/MigrationTools/Processors/Infrastructure/ProcessorOptions.cs @@ -9,7 +9,7 @@ namespace MigrationTools.Processors.Infrastructure { - public abstract class ProcessorOptions : IProcessorOptions + public abstract class ProcessorOptions : IProcessorOptions, IValidateOptions { [JsonIgnore] public string OptionFor => $"{GetType().Name.Replace("Options", "")}"; @@ -54,5 +54,25 @@ public bool IsProcessorCompatible(IReadOnlyList otherProcessor { return true; } + + public ValidateOptionsResult Validate(string name, ProcessorOptions options) + { + var errors = new List(); + if (string.IsNullOrWhiteSpace(options.SourceName)) + { + errors.Add("The `SourceName` field on {name} must contain a value that matches one of the Endpoint names."); + } + if (string.IsNullOrWhiteSpace(options.TargetName)) + { + errors.Add("The `TargetName` field on {name} must contain a value that matches one of the Endpoint names."); + } + + if (errors.Count > 0) + { + return ValidateOptionsResult.Fail(errors); + } + + return ValidateOptionsResult.Success; + } } } \ No newline at end of file