diff --git a/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md b/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md new file mode 100644 index 0000000000..3988f8b48f --- /dev/null +++ b/docs/specs/audit-instance-servicecontrol-queue-address-manual-test-plan.md @@ -0,0 +1,282 @@ +# Manual Test Plan: Audit Instance ServiceControl Queue Address (SCMU) + +> [!WARNING] +> **This document is a working artifact for the manual validation of this feature and +> is meant to be DELETED before merging the PR.** Record the test results here (or in +> a copy) while the PR is open. + +**Spec:** [audit-instance-servicecontrol-queue-address.md](audit-instance-servicecontrol-queue-address.md) +**Bug:** [#4753 — SCMU does not set ServiceControlQueueAddress when only adding audit instances](https://github.com/Particular/ServiceControl/issues/4753) + +This plan validates, end-to-end on a real machine, the rules implemented in the SCMU +"Add ServiceControl" screen. The automated acceptance tests cover the view model and +validator; this plan covers what those tests cannot: the actual Windows service +installation and the setting written to `ServiceControl.Audit.exe.config`. + +Scenarios are independent of each other and can be executed together (mob-style), +split among testers, or run async. Each scenario states its own preconditions, so any +tester can bring a machine into the required state without running previous scenarios. + +--- + +## Shared machine setup (once per test machine) + +All scenarios need this baseline. If you split scenarios among testers on different +machines, each machine needs this setup. + +1. **Windows machine or VM** with local administrator rights (SCMU requires elevation + to install Windows services). +2. **Build and launch the SCMU under test** from this branch: + - Build `ServiceControl.Config` (or use the packaged installer produced from the branch). + - Run it elevated. +3. **A working transport** available to the machine, so instances can actually be + installed and started. Any supported transport works; suggested low-friction options: + - RabbitMQ in a local Docker container, or + - SQL Server / PostgreSQL transport against a local instance. + Keep the connection string at hand; every scenario that installs an instance needs it. +4. **Snapshot/cleanup strategy:** scenarios require specific numbers of pre-installed + error instances (0, 1, or 2). Between scenarios, remove instances via SCMU + (instance → Remove, tick "delete database" and "delete logs") or restore a VM + snapshot of the clean baseline. Removal order does not matter for this plan. + +### How to verify the written setting (used by several scenarios) + +1. Open the audit instance's install path (visible on the instance card in SCMU), + e.g. `C:\ProgramData\Particular\ServiceControl\\` or the destination + path chosen during install. +2. Open `ServiceControl.Audit.exe.config`. +3. Locate ``. +4. The `value` must be exactly the expected error instance name for the scenario. +5. Additionally verify the audit instance **starts and stays running** (SCMU shows it + running; the instance log contains no + ["no destination specified for message"](https://docs.particular.net/servicecontrol/troubleshooting#no-destination-specified-for-message) + errors). + +### Terminology + +- **Error instance** = a "ServiceControl" instance (the SCMU add screen offers + "ServiceControl" and "ServiceControl Audit"; the first is referred to as the error + instance in the UI validation messages). +- **Add screen** = SCMU → New → "Add ServiceControl and Audit Instances". + +--- + +## Scenario matrix + +| # | Rule | Scenario | Pre-installed error instances required | +|---|------|----------|----------------------------------------| +| 1 | Rule 1 | Install both together → audit points at the new error instance | 0 | +| 2 | Rule 1 (counter) | Install both together with existing error instances → new error instance still wins, no dropdown | 2 | +| 3 | Rule 2 | Add audit alone, one existing error instance → auto-detected, no dropdown | 1 | +| 4 | Rule 3 | Add audit alone, two existing error instances → dropdown offers both | 2 | +| 5 | Rule 3 | Save blocked until a dropdown choice is made, unblocked after | 2 | +| 6 | Rule 4 | Add audit alone, no error instance → warning shown, install blocked | 0 | +| 7 | Rule 4 (counter) | Add error instance alone → no queue-address validation raised | 0 | + +Suggested split for three testers: A = 1, 2 (both-together flows); B = 3, 6, 7 +(auto-detect and blocking); C = 4, 5 (multiple-instance dropdown). Scenarios 2, 4 and 5 +share the same precondition (two error instances installed), so grouping them on one +machine saves setup time. + +--- + +## Scenario 1 — Rule 1: both instances installed together + +**Rule:** Must address the audit instance to the error instance installed in the same session. + +**Preconditions** +- No ServiceControl (error) or audit instances installed on the machine. +- Verify: SCMU instance list is empty. + +**Steps** +1. Open the Add screen. +2. Keep **both** checkboxes ticked (ServiceControl and ServiceControl Audit). +3. Note the error instance name (default `Particular.ServiceControl`). +4. Fill in transport, connection string, ports, and paths as needed. +5. Observe the audit section: no "ERROR INSTANCE" dropdown and no red + "No error instance was found..." warning should be visible. +6. Click Add and let the installation finish. + +**Pass criteria** +- [ ] No dropdown and no warning were shown in the audit section. +- [ ] Installation completes; both instances appear in SCMU and start. +- [ ] `ServiceControl.Audit.exe.config` contains + `ServiceControl.Audit/ServiceControlQueueAddress` = the error instance name from step 3. +- [ ] Audit instance runs with no "no destination specified for message" log errors. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 2 — Rule 1 counter-example: existing instances do not win over the one being installed + +**Rule:** When installing both together, the error instance being installed always wins — never an already-installed one. + +**Preconditions** +- **Two** error instances already installed (e.g. `Particular.ServiceControl.A` and + `Particular.ServiceControl.B`), no audit instances. +- Tip: install them one at a time via the Add screen with only the ServiceControl + checkbox ticked, using distinct names/ports. +- Verify: SCMU lists exactly two error instances. + +**Steps** +1. Open the Add screen. +2. Keep both checkboxes ticked. +3. Give the new error instance a distinct name (e.g. `Particular.ServiceControl.New`). +4. Observe the audit section: even though two error instances exist, **no** + "ERROR INSTANCE" dropdown should be shown (the instance being installed wins). +5. Fill in the remaining fields and click Add. + +**Pass criteria** +- [ ] No dropdown was shown while both checkboxes were ticked. +- [ ] `ServiceControl.Audit.exe.config` contains + `ServiceControlQueueAddress` = `Particular.ServiceControl.New` (the new instance, + not `...A` or `...B`). +- [ ] Audit instance starts and stays running. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 3 — Rule 2: auto-detect single existing error instance + +**Rule:** Should auto-detect the existing error instance when adding an audit instance alone. + +**Preconditions** +- Exactly **one** error instance installed (note its name, e.g. `Particular.ServiceControl`), + no audit instances. +- Verify: SCMU lists exactly one error instance. + +**Steps** +1. Open the Add screen. +2. **Untick** the ServiceControl checkbox; keep only ServiceControl Audit ticked. +3. Observe the audit section: no "ERROR INSTANCE" dropdown and no warning should be + visible — detection is silent. +4. Fill in the remaining fields and click Add. + +**Pass criteria** +- [ ] No dropdown and no warning were shown; Save was not blocked by the queue address. +- [ ] `ServiceControl.Audit.exe.config` contains + `ServiceControlQueueAddress` = the name of the pre-existing error instance. +- [ ] Audit instance starts and stays running. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 4 — Rule 3: dropdown offers all detected error instances + +**Rule:** Must require an explicit choice when multiple existing error instances are found. + +**Preconditions** +- **Two** error instances installed (e.g. `Particular.ServiceControl.A`, + `Particular.ServiceControl.B`), no audit instances. +- Verify: SCMU lists exactly two error instances. + +**Steps** +1. Open the Add screen. +2. Untick the ServiceControl checkbox; keep only ServiceControl Audit ticked. +3. Locate the "ERROR INSTANCE" dropdown in the audit GENERAL section. +4. Open the dropdown and inspect its items. + +**Pass criteria** +- [ ] The dropdown is visible (only in this configuration — see also scenarios 1-3 + where it must be hidden). +- [ ] The dropdown contains exactly the two installed instance names, no more, no less. +- [ ] No instance is silently pre-selected on first open (no guess is made for the user). + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 5 — Rule 3: Save blocked until a choice is made + +**Rule:** Save is blocked until the user picks one of the detected instances, and unblocked once a choice is made. + +**Preconditions** +- Same as Scenario 4 (two error instances, no audit instances). Can be run in the same + session as Scenario 4. + +**Steps** +1. Open the Add screen; untick ServiceControl, keep ServiceControl Audit ticked. +2. Fill in **all other** required audit fields (name, transport, connection string, + ports, paths, service account) but leave the "ERROR INSTANCE" dropdown unselected. +3. Click Add. +4. Expect a validation error on the dropdown; the installation must not start. +5. Select one of the two instances in the dropdown (note which one, e.g. + `Particular.ServiceControl.B`). +6. Click Add again and let the installation finish. + +**Pass criteria** +- [ ] Step 3: install did not proceed; the validation error + "An existing error instance must be selected for the audit instance to send messages to" + was raised against the dropdown. +- [ ] Step 6: install proceeded after choosing. +- [ ] `ServiceControl.Audit.exe.config` contains `ServiceControlQueueAddress` = the + instance chosen in step 5. +- [ ] Audit instance starts and stays running. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 6 — Rule 4: install blocked when no error instance exists + +**Rule:** Must block installation when no error instance exists to connect to. + +**Preconditions** +- **No** error instances installed (audit instances, if any, removed as well for a + clean read of the instance list). +- Verify: SCMU instance list contains no error instances. + +**Steps** +1. Open the Add screen. +2. Untick the ServiceControl checkbox; keep only ServiceControl Audit ticked. +3. Observe the audit GENERAL section. +4. Fill in all required audit fields and click Add. + +**Pass criteria** +- [ ] The red warning is shown: "No error instance was found on this machine. The + audit instance needs one to send messages to. Also install the error instance + above, or add one first." +- [ ] No "ERROR INSTANCE" dropdown is shown (there is nothing to choose from). +- [ ] Clicking Add does not install anything; a validation error blocks the save. +- [ ] Bonus check: ticking the ServiceControl checkbox again makes the warning + disappear (the error instance in the same session satisfies the requirement). + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Scenario 7 — Rule 4 counter-example: error-instance-only install is unaffected + +**Rule:** When only an error instance is being installed, the queue address does not apply and no validation error is raised. + +**Preconditions** +- No instances installed. +- Verify: SCMU instance list is empty. + +**Steps** +1. Open the Add screen. +2. Untick the ServiceControl **Audit** checkbox; keep only ServiceControl ticked. +3. Fill in the error instance fields and click Add. + +**Pass criteria** +- [ ] No queue-address validation error appears anywhere in the flow. +- [ ] The error instance installs and starts normally. + +**Result:** ☐ Pass ☐ Fail — Tester: ______ Date: ______ Notes: ______ + +--- + +## Wrap-up + +- Collect results per scenario (pass/fail, tester, notes) in this file or a copy. +- Any failure: capture a screenshot of the Add screen, the SCMU log, and the + `ServiceControl.Audit.exe.config` (if written), and link them in the notes. +- Known out-of-scope behavior (do **not** log as failures): + - The new audit instance is *not* registered as a remote of a pre-existing error + instance (`AddRemoteInstance` only runs when both are installed together) — + tracked as a follow-up candidate in the spec. + - PowerShell (`New-ServiceControlAuditInstance`) already enforces the parameter as + mandatory and is not covered by this plan. diff --git a/docs/specs/audit-instance-servicecontrol-queue-address.md b/docs/specs/audit-instance-servicecontrol-queue-address.md new file mode 100644 index 0000000000..e8841e53de --- /dev/null +++ b/docs/specs/audit-instance-servicecontrol-queue-address.md @@ -0,0 +1,75 @@ +# Feature: Audit Instance ServiceControl Queue Address + +> [!WARNING] +> **This document is a working artifact for the development of this feature and is +> meant to be DELETED before merging the PR.** Do not reference it from code or docs +> that will outlive the PR. + +**As an operator adding instances through ServiceControl Management Utility (SCMU), I want every audit instance to be connected to a ServiceControl (error) instance so that messages the audit instance sends to the error instance always have a valid destination.** + +> Bug: [#4753 — SCMU does not set ServiceControlQueueAddress when only adding audit instances](https://github.com/Particular/ServiceControl/issues/4753). +> When the `ServiceControl.Audit/ServiceControlQueueAddress` setting is missing from +> `ServiceControl.Audit.exe.config`, the audit instance fails at runtime with +> ["no destination specified for message"](https://docs.particular.net/servicecontrol/troubleshooting#no-destination-specified-for-message). + +## Rules and Examples + +### Rule 1: Must address the audit instance to the error instance installed in the same session + +When the user installs an error instance and an audit instance together, the audit +instance's queue address is the name of the error instance being installed — never an +already-installed one. + +- **Example:** The one where both instances are installed together and the audit + instance's queue address is the new error instance's name. +- **Counter-example:** The one where other error instances already exist on the + machine, yet no choice is offered — the error instance being installed always wins. + +--- + +### Rule 2: Should auto-detect the existing error instance when adding an audit instance alone + +- **Example:** The one where exactly one error instance exists on the machine and its + name is used as the queue address without any user input (no dropdown shown). + +--- + +### Rule 3: Must require an explicit choice when multiple existing error instances are found + +Auto-detection cannot guess between several error instances — picking one silently +risks routing messages to the wrong instance. The choice dropdown is shown **only** in +this case. + +- **Example:** The one where two error instances exist and the dropdown offers both. +- **Example:** The one where Save is blocked until the user picks one of the detected + instances, and unblocked once a choice is made. + +--- + +### Rule 4: Must block installation when no error instance exists to connect to + +An audit instance without a reachable error instance is misconfigured by definition; +SCMU must not produce it. + +- **Example:** The one where no error instance exists, the user adds an audit instance + alone, and a validation error prevents the installation from proceeding. +- **Counter-example:** The one where only an error instance is being installed — the + queue address does not apply and no validation error is raised. + +## Resolved decisions (for implementation) + +- **Auto-detect source:** installed Windows error instances, discovered via + `InstanceFinder.ServiceControlInstances()`; exposed on the view model through a + `GetInstalledErrorInstanceNames` function seam (mirrors the existing + `GetWindowsServiceNames` pattern) so tests can substitute it. +- **Multiple instances found:** user must choose from a dropdown that is visible only + when adding an audit instance alone **and** more than one error instance is detected. +- **No instance found:** Save is blocked by a validation error; deploying with + PowerShell remains the path for advanced scenarios. +- **Acceptance tier:** view model + validator observed through `INotifyDataErrorInfo` + — the same mechanism the UI uses to block Save. A full SCMU end-to-end test (install + a Windows service, inspect the written config file) is not automatable in this + repository's test suites. +- **Out of scope:** registering the new audit instance as a remote of the existing + error instance (`AddRemoteInstance` is only called when both instances are installed + together) — candidate for a follow-up issue. diff --git a/src/ServiceControl.Config.Tests/.editorconfig b/src/ServiceControl.Config.Tests/.editorconfig index 5f68a610b3..c5410d8c02 100644 --- a/src/ServiceControl.Config.Tests/.editorconfig +++ b/src/ServiceControl.Config.Tests/.editorconfig @@ -2,3 +2,7 @@ # Justification: Test project dotnet_diagnostic.CA2007.severity = none + +# Justification: Executable specifications intentionally assign properties after the +# object initializer to mirror user interaction order (e.g. typing a name after load) +dotnet_diagnostic.IDE0017.severity = none diff --git a/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs b/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs new file mode 100644 index 0000000000..bfd43554ac --- /dev/null +++ b/src/ServiceControl.Config.Tests/AddInstance/AuditInstanceServiceControlQueueAddress.cs @@ -0,0 +1,195 @@ +namespace ServiceControl.Config.Tests.AddInstance +{ + using System.ComponentModel; + using NUnit.Framework; + using ServiceControl.Config.UI.InstanceAdd; + + /// + /// Executable specification for the audit instance ServiceControl queue address feature + /// (bug https://github.com/Particular/ServiceControl/issues/4753). + /// + /// Organized as feature > rule > examples: + /// - this outer class is the feature, + /// - each nested fixture is one rule, + /// - each test is one example, named with "The one where ..." language. + /// + /// The tests observe the view model and its validator through INotifyDataErrorInfo - + /// the same mechanism the UI uses to block Save - and substitute the + /// GetInstalledErrorInstanceNames seam to simulate the error instances installed on + /// the machine. + /// + public class AuditInstanceServiceControlQueueAddress + { + [TestFixture] + public class Rule_1_Must_address_the_audit_instance_to_the_error_instance_installed_in_the_same_session + { + [Test] + public void The_one_where_both_instances_are_installed_together_and_the_new_error_instance_name_is_used() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = true, + SubmitAttempted = true, + // No pre-existing error instances on the machine + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.ErrorInstanceName = "My.Error.Instance"; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("My.Error.Instance")); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + [Test] + public void The_one_where_other_error_instances_already_exist_yet_no_choice_is_offered_because_the_instance_being_installed_wins() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + } + } + + [TestFixture] + public class Rule_2_Should_auto_detect_the_existing_error_instance_when_adding_an_audit_instance_alone + { + [Test] + public void The_one_where_a_single_error_instance_exists_and_its_name_is_used_without_any_user_input() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl" } + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl")); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False, "Dropdown must not show when there is only one existing error instance"); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + } + + [TestFixture] + public class Rule_3_Must_require_an_explicit_choice_when_multiple_existing_error_instances_are_found + { + [Test] + public void The_one_where_two_error_instances_exist_and_the_dropdown_offers_both() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.True); + Assert.That(viewModel.ServiceControlQueueAddressOptions, Is.EquivalentTo(new[] + { + "Particular.ServiceControl", + "Particular.ServiceControl.2" + })); + } + } + + [Test] + public void The_one_where_save_is_blocked_until_the_user_picks_one_of_the_detected_instances() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[] { "Particular.ServiceControl", "Particular.ServiceControl.2" } + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + // No selection made yet: must be blocked + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, + "A validation error is expected until the user picks one of the existing error instances"); + + // User picks an instance from the dropdown + viewModel.ServiceControlQueueAddress = "Particular.ServiceControl.2"; + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl.2")); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + } + + [TestFixture] + public class Rule_4_Must_block_installation_when_no_error_instance_exists_to_connect_to + { + [Test] + public void The_one_where_no_error_instance_exists_and_a_validation_error_prevents_the_installation_from_proceeding() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = false, + InstallAuditInstance = true, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + using (Assert.EnterMultipleScope()) + { + Assert.That(viewModel.ServiceControlQueueAddress, Is.Null.Or.Empty); + Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False); + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty, + "A validation error is expected so the user cannot proceed without an existing error instance to connect to"); + } + } + + [Test] + public void The_one_where_only_an_error_instance_is_installed_and_the_queue_address_does_not_apply() + { + var viewModel = new ServiceControlAddViewModel + { + InstallErrorInstance = true, + InstallAuditInstance = false, + SubmitAttempted = true, + GetInstalledErrorInstanceNames = () => new string[0] + }; + + viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress)); + + var notifyErrorInfo = GetNotifyErrorInfo(viewModel); + + Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty); + } + } + + static INotifyDataErrorInfo GetNotifyErrorInfo(object vm) => vm as INotifyDataErrorInfo; + } +} diff --git a/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs b/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs index 118d0bc687..3c97a982ec 100644 --- a/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs +++ b/src/ServiceControl.Config.Tests/ServiceControlAddScreenLoadedTests.cs @@ -153,7 +153,7 @@ public void Database_maintenance_port_number_are_set_to_defaults_with_no_validat [Test] public void Destination_path_is_null() { - var viewModel = new ServiceControlAddViewModel(); + var viewModel = new ServiceControlAddViewModel(() => []); var errorInfo = (INotifyDataErrorInfo)viewModel; @@ -169,7 +169,7 @@ public void Destination_path_is_null() [Test] public void Log_path_is_null() { - var viewModel = new ServiceControlAddViewModel(); + var viewModel = new ServiceControlAddViewModel(() => []); var errorInfo = (INotifyDataErrorInfo)viewModel; @@ -186,7 +186,7 @@ public void Log_path_is_null() [Test] public void Database_path_is_null() { - var viewModel = new ServiceControlAddViewModel(); + var viewModel = new ServiceControlAddViewModel(() => []); var errorInfo = (INotifyDataErrorInfo)viewModel; diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs index 1d3c08009d..5c9863dd03 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddAttachment.cs @@ -100,7 +100,7 @@ async Task Add() auditNewInstance.AuditRetentionPeriod = viewModel.ServiceControlAudit.AuditRetentionPeriod; auditNewInstance.ServiceAccount = viewModel.ServiceControlAudit.ServiceAccount; auditNewInstance.ServiceAccountPwd = viewModel.ServiceControlAudit.Password; - auditNewInstance.ServiceControlQueueAddress = serviceControlNewInstance == null ? string.Empty : serviceControlNewInstance.InstanceName; + auditNewInstance.ServiceControlQueueAddress = viewModel.ServiceControlQueueAddress; auditNewInstance.EnableFullTextSearchOnBodies = viewModel.ServiceControlAudit.EnableFullTextSearchOnBodies.Value; } diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml index 9ace3fbd7a..58c8a3efed 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddView.xaml @@ -299,6 +299,21 @@ + + + + diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs index cc8bdb3d2a..ce26263a1b 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModel.cs @@ -1,4 +1,4 @@ -namespace ServiceControl.Config.UI.InstanceAdd +namespace ServiceControl.Config.UI.InstanceAdd { using System; using System.Collections.Generic; @@ -8,16 +8,21 @@ using System.Windows.Input; using PropertyChanged; using ServiceControl.Config.Extensions; + using ServiceControlInstaller.Engine.Instances; using Validar; using Xaml.Controls; [InjectValidation] public class ServiceControlAddViewModel : ServiceControlEditorViewModel { - public ServiceControlAddViewModel() + // The constructor runs the unique-name convention against the machine's Windows + // services, so tests inject a fake here to stay environment-independent; the + // GetWindowsServiceNames property seam is set too late for construction-time logic + public ServiceControlAddViewModel(Func getWindowsServiceNames = null) { DisplayName = "ADD SERVICECONTROL"; - GetWindowsServiceNames = () => ServiceController.GetServices().Select(windowsService => windowsService.ServiceName).ToArray(); + GetWindowsServiceNames = getWindowsServiceNames ?? (() => ServiceController.GetServices().Select(windowsService => windowsService.ServiceName).ToArray()); + GetInstalledErrorInstanceNames = () => InstanceFinder.ServiceControlInstances().Select(instance => instance.Name).ToArray(); ConventionName = "Particular.ServiceControl"; OnConventionNameChanged(); @@ -30,6 +35,18 @@ public ServiceControlAddViewModel() ServiceControl.PropertyChanged += ServiceControl_PropertyChanged; ServiceControlAudit.PropertyChanged += ServiceControl_PropertyChanged; + PropertyChanged += (_, e) => + { + // InstallErrorInstance/InstallAuditInstance live on the base class, so Fody + // cannot infer that these derived computed properties depend on them + if (e.PropertyName is nameof(InstallErrorInstance) or nameof(InstallAuditInstance)) + { + NotifyOfPropertyChange(nameof(ServiceControlQueueAddress)); + NotifyOfPropertyChange(nameof(ServiceControlQueueAddressOptions)); + NotifyOfPropertyChange(nameof(ShowServiceControlQueueAddressSelection)); + NotifyOfPropertyChange(nameof(ShowNoErrorInstanceFoundWarning)); + } + }; } void ServiceControl_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -47,6 +64,40 @@ void ServiceControl_PropertyChanged(object sender, System.ComponentModel.Propert public Func GetWindowsServiceNames { get; set; } + public Func GetInstalledErrorInstanceNames { get; set; } + + public string[] ServiceControlQueueAddressOptions => GetInstalledErrorInstanceNames(); + + public string ServiceControlQueueAddress + { + get + { + if (InstallErrorInstance) + { + return ErrorInstanceName; + } + + var installedErrorInstanceNames = GetInstalledErrorInstanceNames(); + + return installedErrorInstanceNames.Length == 1 + ? installedErrorInstanceNames[0] + : serviceControlQueueAddress; + } + set => serviceControlQueueAddress = value; + } + + string serviceControlQueueAddress; + + public bool ShowServiceControlQueueAddressSelection => + InstallAuditInstance + && !InstallErrorInstance + && GetInstalledErrorInstanceNames().Length > 1; + + public bool ShowNoErrorInstanceFoundWarning => + InstallAuditInstance + && !InstallErrorInstance + && GetInstalledErrorInstanceNames().Length == 0; + public string ConventionName { get; set; } public void OnConventionNameChanged() diff --git a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs index 7a627d954a..e60e132b7e 100644 --- a/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs +++ b/src/ServiceControl.Config/UI/InstanceAdd/ServiceControlAddViewModelValidator.cs @@ -150,6 +150,11 @@ public ServiceControlAddViewModelValidator() .WithMessage("Audit instance name is already in use") .When(viewModel => viewModel.InstallAuditInstance); + RuleFor(viewModel => viewModel.ServiceControlQueueAddress) + .NotEmpty() + .WithMessage("An existing error instance must be selected for the audit instance to send messages to") + .When(viewModel => viewModel.InstallAuditInstance && !viewModel.InstallErrorInstance); + RuleFor(x => x.AuditServiceAccount) .NotEmpty() .When(x => x.InstallAuditInstance && x.SubmitAttempted);