From 7db33473333948f622b5e59a9a0a7699aec5c943 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 22 Dec 2025 22:17:57 -0700 Subject: [PATCH 1/5] # Fix hostname validation in SCMU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Fix hostname validation in SCMU ## Summary This PR completes the fix for issue #5190 by adding proper hostname validation to ServiceControl.Config. While PR #5238 partially addressed the issue by allowing dashes in hostnames, it didn't implement comprehensive hostname validation, leaving users able to enter invalid hostnames that would cause configuration issues. ## Background **Original Issue**: #5190 reported that ServiceControl.Config was not properly validating hostname fields, allowing users to enter invalid values that would cause runtime failures. **Previous Fix**: PR #5238 addressed part of the problem by allowing dashes in hostnames, but the validation was still incomplete and didn't prevent other invalid hostname formats. **This Fix**: Implements comprehensive RFC-compliant hostname validation using .NET's built-in `Uri.CheckHostName()` method. ## Validation Behavior The new validation accepts: - ✅ Valid DNS hostnames (server01, myhost.domain.com) - ✅ localhost - ✅ Hostnames with dashes (server-01, my-host.domain.com) - *previously fixed by PR #5238* --- .../InstanceAdd/ServiceControlAddViewModelValidator.cs | 2 ++ .../ServiceControlAuditEditViewModelValidator.cs | 1 + .../ServiceControlEditViewModelValidator.cs | 1 + .../SharedMonitoringEditorViewModelValidator.cs | 1 + .../Upgrades/AddNewAuditInstanceViewModelValidator.cs | 5 +++++ src/ServiceControl.Config/Validation/Validations.cs | 10 ++++++++++ 6 files changed, 20 insertions(+) diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs index 223d5505dd..7a627d954a 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs @@ -52,6 +52,7 @@ public ServiceControlAddViewModelValidator() RuleFor(viewModel => viewModel.ErrorHostName) .NotEmpty() + .ValidHostname() .When(viewModel => viewModel.InstallErrorInstance); RuleFor(x => x.ErrorPortNumber) @@ -155,6 +156,7 @@ public ServiceControlAddViewModelValidator() RuleFor(viewModel => viewModel.AuditHostName) .NotEmpty() + .ValidHostname() .When(viewModel => viewModel.InstallAuditInstance); RuleFor(x => x.AuditPortNumber) diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditViewModelValidator.cs index b4dd2524e0..9741d57238 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlAuditEditViewModelValidator.cs @@ -14,6 +14,7 @@ public ServiceControlAuditEditViewModelValidator() RuleFor(x => x.HostName) .NotEmpty() + .ValidHostname() .When(x => x.SubmitAttempted); RuleFor(x => x.PortNumber) diff --git a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModelValidator.cs index d91d0f78da..0e9f6bfe8f 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/ServiceControlEditViewModelValidator.cs @@ -14,6 +14,7 @@ public ServiceControlEditViewModelValidator() RuleFor(x => x.HostName) .NotEmpty() + .ValidHostname() .When(x => x.SubmitAttempted); RuleFor(x => x.PortNumber) diff --git a/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs b/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs index 54d21952ff..3acff6c9f6 100644 --- a/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs @@ -9,6 +9,7 @@ protected SharedMonitoringEditorViewModelValidator() { RuleFor(x => x.HostName) .NotEmpty() + .ValidHostname() .When(x => x.SubmitAttempted); RuleFor(x => x.LogPath) diff --git a/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceViewModelValidator.cs b/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceViewModelValidator.cs index cd38ec6059..26b4cdc499 100644 --- a/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/Upgrades/AddNewAuditInstanceViewModelValidator.cs @@ -60,6 +60,11 @@ public AddNewAuditInstanceViewModelValidator() .NotEmpty() .NotEqual(x => x.ServiceControlAudit.AuditQueueName).WithMessage(string.Format(Validation.Validations.MSG_UNIQUEQUEUENAME, "Audit")) .When(x => x.SubmitAttempted && (x.ServiceControlAudit.AuditForwarding?.Value ?? false)); + + RuleFor(x => x.ServiceControlAudit.HostName) + .NotEmpty() + .ValidHostname() + .When(x => x.SubmitAttempted); } } } \ No newline at end of file diff --git a/src/ServiceControl.Config/Validation/Validations.cs b/src/ServiceControl.Config/Validation/Validations.cs index b0562eee77..e6ac3a471b 100644 --- a/src/ServiceControl.Config/Validation/Validations.cs +++ b/src/ServiceControl.Config/Validation/Validations.cs @@ -202,6 +202,14 @@ public static IRuleBuilderOptions MustBeUniqueWindowsServiceName(t }); } + public static IRuleBuilderOptions ValidHostname(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must((t, hostname) => + !string.IsNullOrWhiteSpace(hostname) && + Uri.CheckHostName(hostname) == UriHostNameType.Dns) + .WithMessage(MSG_INVALID_HOSTNAME); + } + public const string MSG_EMAIL_NOT_VALID = "Not Valid."; public const string MSG_THIS_TRANSPORT_REQUIRES_A_CONNECTION_STRING = "This transport requires a connection string."; @@ -234,6 +242,8 @@ public static IRuleBuilderOptions MustBeUniqueWindowsServiceName(t public const string MSG_ILLEGAL_PATH_CHAR = "Paths cannot contain characters {0}"; + public const string MSG_INVALID_HOSTNAME = "Hostname is not valid."; + static char[] ILLEGAL_PATH_CHARS = { '*', From 25073c2fd757647700dc91c188e60eed8325350b Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 22 Dec 2025 23:24:22 -0700 Subject: [PATCH 2/5] test case for IPV6 --- .../Validation/AddAuditInstanceValidationTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs index d76f3400fc..4c5f99514a 100644 --- a/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs @@ -321,6 +321,8 @@ public void Audit_hostname_can_not_be_null_when_adding_audit_instance() [TestCase("192.168.1.1")] [TestCase("256.0.0.0")] + [TestCase("::1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] public void Audit_hostname_can_be_an_ip_address_when_adding_audit_instance(string ipAddress) { var viewModel = new ServiceControlAddViewModel From 8750593a66049109f6307885dbbb0c9a15c942f7 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 22 Dec 2025 23:24:44 -0700 Subject: [PATCH 3/5] fixing broken implementation for ips --- .../MonitoringEditViewModelValidator.cs | 4 ---- .../SharedMonitoringEditorViewModelValidator.cs | 3 +-- src/ServiceControl.Config/Validation/Validations.cs | 13 ++++++++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditViewModelValidator.cs index d9c762282c..46aad9154d 100644 --- a/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceEdit/MonitoringEditViewModelValidator.cs @@ -12,10 +12,6 @@ public MonitoringEditViewModelValidator() .NotEmpty() .When(x => x.SubmitAttempted); - RuleFor(x => x.HostName) - .NotEmpty() - .When(x => x.SubmitAttempted); - RuleFor(x => x.PortNumber) .NotEmpty() .ValidPort() diff --git a/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs b/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs index 3acff6c9f6..5ac92a53c5 100644 --- a/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs @@ -9,8 +9,7 @@ protected SharedMonitoringEditorViewModelValidator() { RuleFor(x => x.HostName) .NotEmpty() - .ValidHostname() - .When(x => x.SubmitAttempted); + .ValidHostname(); // Removed the .When(x => x.SubmitAttempted) condition RuleFor(x => x.LogPath) .NotEmpty() diff --git a/src/ServiceControl.Config/Validation/Validations.cs b/src/ServiceControl.Config/Validation/Validations.cs index e6ac3a471b..7fb7ba8373 100644 --- a/src/ServiceControl.Config/Validation/Validations.cs +++ b/src/ServiceControl.Config/Validation/Validations.cs @@ -205,9 +205,16 @@ public static IRuleBuilderOptions MustBeUniqueWindowsServiceName(t public static IRuleBuilderOptions ValidHostname(this IRuleBuilder ruleBuilder) { return ruleBuilder.Must((t, hostname) => - !string.IsNullOrWhiteSpace(hostname) && - Uri.CheckHostName(hostname) == UriHostNameType.Dns) - .WithMessage(MSG_INVALID_HOSTNAME); + { + if (string.IsNullOrWhiteSpace(hostname)) + { + return false; + } + + var hostNameType = Uri.CheckHostName(hostname); + return hostNameType is UriHostNameType.Dns or UriHostNameType.IPv4 or UriHostNameType.IPv6; + }) + .WithMessage(MSG_INVALID_HOSTNAME); } public const string MSG_EMAIL_NOT_VALID = "Not Valid."; From c8864350cc1f3d8c8beeb6329e3a9ba508eb469a Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 23 Dec 2025 01:36:21 -0700 Subject: [PATCH 4/5] adding missing test cases --- .../AddAuditInstanceValidationTests.cs | 27 +++++++++++++++++ .../AddErrorInstanceValidationTests.cs | 29 +++++++++++++++++++ .../AddMonitoringInstanceValidationTests.cs | 28 ++++++++++++++++++ .../EditAuditInstanceValidationTests.cs | 29 +++++++++++++++++++ .../EditErrorInstanceValidationTests.cs | 29 +++++++++++++++++++ .../EditMonitoringInstanceValidationTests.cs | 29 +++++++++++++++++++ ...haredMonitoringEditorViewModelValidator.cs | 2 +- 7 files changed, 172 insertions(+), 1 deletion(-) diff --git a/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs index 4c5f99514a..800976d87a 100644 --- a/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/AddAuditInstanceValidationTests.cs @@ -339,6 +339,33 @@ public void Audit_hostname_can_be_an_ip_address_when_adding_audit_instance(strin Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.AuditHostName)), Is.Empty); } + [TestCase("hostname with spaces")] + [TestCase("bad@hostname")] + [TestCase("bad#hostname")] + [TestCase("badhostname...")] + [TestCase("badhostname[/")] + public void Audit_hostname_cannot_contain_invalid_characters_when_adding_audit_instance(string invalidHostname) + { + var viewModel = new ServiceControlAddViewModel + { + InstallAuditInstance = true, + SubmitAttempted = true, + AuditHostName = invalidHostname + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.AuditHostName)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + var errors = notifyErrorInfo.GetErrors(nameof(viewModel.AuditHostName)); + + Assert.Multiple(() => + { + Assert.That(errors, Is.Not.Empty, "Hostname validation should exist and trigger for invalid hostnames"); + Assert.That(errors.Cast().Any(error => error.Contains("Hostname is not valid")), Is.True, + "Hostname validation should display the exact error message 'Hostname is not valid'"); + }); + } + #endregion #region Portnumber diff --git a/src/ServiceControl.Config.Tests/Validation/AddErrorInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/AddErrorInstanceValidationTests.cs index 534ba7f705..12150cab65 100644 --- a/src/ServiceControl.Config.Tests/Validation/AddErrorInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/AddErrorInstanceValidationTests.cs @@ -320,6 +320,8 @@ public void Error_hostname_can_not_be_null_when_adding_error_instance() [TestCase("192.168.1.1")] [TestCase("256.0.0.0")] + [TestCase("::1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] public void Error_hostname_can_be_an_ip_address_when_adding_error_instance(string ipAddress) { var viewModel = new ServiceControlAddViewModel @@ -336,6 +338,33 @@ public void Error_hostname_can_be_an_ip_address_when_adding_error_instance(strin Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ErrorHostName)), Is.Empty); } + [TestCase("hostname with spaces")] + [TestCase("bad@hostname")] + [TestCase("bad#hostname")] + [TestCase("badhostname...")] + [TestCase("badhostname[/")] + public void Error_hostname_cannot_contain_invalid_characters_when_adding_error_instance(string invalidHostname) + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + SubmitAttempted = true, + ErrorHostName = invalidHostname + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ErrorHostName)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + var errors = notifyErrorInfo.GetErrors(nameof(viewModel.ErrorHostName)); + + Assert.Multiple(() => + { + Assert.That(errors, Is.Not.Empty, "Hostname validation should exist and trigger for invalid hostnames"); + Assert.That(errors.Cast().Any(error => error.Contains("Hostname is not valid")), Is.True, + "Hostname validation should display the exact error message 'Hostname is not valid'"); + }); + } + #endregion #region Portnumber diff --git a/src/ServiceControl.Config.Tests/Validation/AddMonitoringInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/AddMonitoringInstanceValidationTests.cs index 42ccf0e6dd..5a089ddee9 100644 --- a/src/ServiceControl.Config.Tests/Validation/AddMonitoringInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/AddMonitoringInstanceValidationTests.cs @@ -127,6 +127,8 @@ public void Monitoring_hostname_cannot_be_null_when_adding_monitoring_instance() [TestCase("192.168.1.1")] [TestCase("256.0.0.0")] + [TestCase("::1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] public void Monitoring_hostname_can_be_an_ip_address_when_adding_monitoring_instance(string ipAddress) { var viewModel = new MonitoringAddViewModel @@ -142,6 +144,32 @@ public void Monitoring_hostname_can_be_an_ip_address_when_adding_monitoring_inst Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.HostName)), Is.Empty); } + [TestCase("hostname with spaces")] + [TestCase("bad@hostname")] + [TestCase("bad#hostname")] + [TestCase("badhostname...")] + [TestCase("badhostname[/")] + public void Monitoring_hostname_cannot_contain_invalid_characters_when_adding_monitoring_instance(string invalidHostname) + { + var viewModel = new MonitoringAddViewModel + { + SubmitAttempted = true, + HostName = invalidHostname + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.HostName)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + var errors = notifyErrorInfo.GetErrors(nameof(viewModel.HostName)); + + Assert.Multiple(() => + { + Assert.That(errors, Is.Not.Empty, "Hostname validation should exist and trigger for invalid hostnames"); + Assert.That(errors.Cast().Any(error => error.Contains("Hostname is not valid")), Is.True, + "Hostname validation should display the exact error message 'Hostname is not valid'"); + }); + } + #endregion #region Portnumber diff --git a/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs index 73abc4d6d2..5212c9d36c 100644 --- a/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Tests.Validation { using System.ComponentModel; + using System.Linq; using NUnit.Framework; using ServiceControlInstaller.Engine.Instances; using UI.InstanceAdd; @@ -127,6 +128,8 @@ public void Audit_hostname_can_not_be_null_when_editing_audit_instance() [TestCase("192.168.1.1")] [TestCase("256.0.0.0")] + [TestCase("::1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] public void Audi_hostname_can_be_an_ip_address_when_editing_an_audit_instance(string ipAddress) { var viewModel = new ServiceControlAuditEditViewModel @@ -142,6 +145,32 @@ public void Audi_hostname_can_be_an_ip_address_when_editing_an_audit_instance(st Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.HostName)), Is.Empty); } + [TestCase("hostname with spaces")] + [TestCase("bad@hostname")] + [TestCase("bad#hostname")] + [TestCase("badhostname...")] + [TestCase("badhostname[/")] + public void Audit_hostname_cannot_contain_invalid_characters_when_editing_audit_instance(string invalidHostname) + { + var viewModel = new ServiceControlAuditEditViewModel + { + SubmitAttempted = true, + HostName = invalidHostname + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.HostName)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + var errors = notifyErrorInfo.GetErrors(nameof(viewModel.HostName)); + + Assert.Multiple(() => + { + Assert.That(errors, Is.Not.Empty, "Hostname validation should exist and trigger for invalid hostnames"); + Assert.That(errors.Cast().Any(error => error.Contains("Hostname is not valid")), Is.True, + "Hostname validation should display the exact error message 'Hostname is not valid'"); + }); + } + #endregion #region Portnumber diff --git a/src/ServiceControl.Config.Tests/Validation/EditErrorInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/EditErrorInstanceValidationTests.cs index a1e744191a..230bf2bbad 100644 --- a/src/ServiceControl.Config.Tests/Validation/EditErrorInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/EditErrorInstanceValidationTests.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Tests.Validation { using System.ComponentModel; + using System.Linq; using NUnit.Framework; using ServiceControlInstaller.Engine.Instances; using UI.InstanceAdd; @@ -120,6 +121,8 @@ public void Error_hostname_can_not_be_null_when_editing_error_instance() [TestCase("192.168.1.1")] [TestCase("256.0.0.0")] + [TestCase("::1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] public void Error_hostname_can_be_an_ip_address_when_editing_an_error_instance(string ipAddress) { var viewModel = new ServiceControlEditViewModel @@ -135,6 +138,32 @@ public void Error_hostname_can_be_an_ip_address_when_editing_an_error_instance(s Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.HostName)), Is.Empty); } + [TestCase("hostname with spaces")] + [TestCase("bad@hostname")] + [TestCase("bad#hostname")] + [TestCase("badhostname...")] + [TestCase("badhostname[/")] + public void Error_hostname_cannot_contain_invalid_characters_when_editing_error_instance(string invalidHostname) + { + var viewModel = new ServiceControlEditViewModel + { + SubmitAttempted = true, + HostName = invalidHostname + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.HostName)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + var errors = notifyErrorInfo.GetErrors(nameof(viewModel.HostName)); + + Assert.Multiple(() => + { + Assert.That(errors, Is.Not.Empty, "Hostname validation should exist and trigger for invalid hostnames"); + Assert.That(errors.Cast().Any(error => error.Contains("Hostname is not valid")), Is.True, + "Hostname validation should display the exact error message 'Hostname is not valid'"); + }); + } + #endregion #region Portnumber diff --git a/src/ServiceControl.Config.Tests/Validation/EditMonitoringInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/EditMonitoringInstanceValidationTests.cs index 18815e2ea7..2754b0dd87 100644 --- a/src/ServiceControl.Config.Tests/Validation/EditMonitoringInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/EditMonitoringInstanceValidationTests.cs @@ -1,6 +1,7 @@ namespace ServiceControl.Config.Tests.Validation { using System.ComponentModel; + using System.Linq; using NUnit.Framework; using UI.InstanceEdit; @@ -42,6 +43,8 @@ public void Monitoring_hostname_cannot_be_null_when_editing_monitoring_instance( [TestCase("192.168.1.1")] [TestCase("256.0.0.0")] + [TestCase("::1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] public void Monitoring_hostname_can_be_an_ip_address_when_editing_a_monitoring_instance(string ipAddress) { var viewModel = new MonitoringEditViewModel @@ -56,6 +59,32 @@ public void Monitoring_hostname_can_be_an_ip_address_when_editing_a_monitoring_i Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.HostName)), Is.Empty); } + + [TestCase("hostname with spaces")] + [TestCase("bad@hostname")] + [TestCase("bad#hostname")] + [TestCase("badhostname...")] + [TestCase("badhostname[/")] + public void Monitoring_hostname_cannot_contain_invalid_characters_when_editing_monitoring_instance(string invalidHostname) + { + var viewModel = new MonitoringEditViewModel + { + SubmitAttempted = true, + HostName = invalidHostname + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.HostName)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + var errors = notifyErrorInfo.GetErrors(nameof(viewModel.HostName)); + + Assert.Multiple(() => + { + Assert.That(errors, Is.Not.Empty, "Hostname validation should exist and trigger for invalid hostnames"); + Assert.That(errors.Cast().Any(error => error.Contains("Hostname is not valid")), Is.True, + "Hostname validation should display the exact error message 'Hostname is not valid'"); + }); + } #endregion #region Portnumber diff --git a/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs b/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs index 5ac92a53c5..d226baa652 100644 --- a/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/SharedInstanceEditor/SharedMonitoringEditorViewModelValidator.cs @@ -9,7 +9,7 @@ protected SharedMonitoringEditorViewModelValidator() { RuleFor(x => x.HostName) .NotEmpty() - .ValidHostname(); // Removed the .When(x => x.SubmitAttempted) condition + .ValidHostname(); RuleFor(x => x.LogPath) .NotEmpty() From 32c6c1537c99c14d6ca5cca13e2c1a9f2eee20d0 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 23 Dec 2025 19:52:38 -0600 Subject: [PATCH 5/5] Update src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs Co-authored-by: Nick Gallegos --- .../Validation/EditAuditInstanceValidationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs b/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs index 5212c9d36c..977bc00b9e 100644 --- a/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs +++ b/src/ServiceControl.Config.Tests/Validation/EditAuditInstanceValidationTests.cs @@ -130,7 +130,7 @@ public void Audit_hostname_can_not_be_null_when_editing_audit_instance() [TestCase("256.0.0.0")] [TestCase("::1")] [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] - public void Audi_hostname_can_be_an_ip_address_when_editing_an_audit_instance(string ipAddress) + public void Audit_hostname_can_be_an_ip_address_when_editing_an_audit_instance(string ipAddress) { var viewModel = new ServiceControlAuditEditViewModel {