Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ServiceControl.Config.Tests/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
namespace ServiceControl.Config.Tests.AddInstance
{
using System.ComponentModel;
using NUnit.Framework;
using ServiceControl.Config.UI.InstanceAdd;

/// <summary>
/// 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.
/// </summary>
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_require_a_manually_entered_queue_address_when_no_error_instance_is_detected
{
[Test]
public void The_one_where_no_error_instance_is_detected_and_save_is_blocked_until_the_remote_address_is_entered()
{
var viewModel = new ServiceControlAddViewModel
{
InstallErrorInstance = false,
InstallAuditInstance = true,
SubmitAttempted = true,
GetInstalledErrorInstanceNames = () => new string[0]
};

viewModel.NotifyOfPropertyChange(nameof(viewModel.ServiceControlQueueAddress));

var notifyErrorInfo = GetNotifyErrorInfo(viewModel);

// Nothing entered yet: must be blocked
using (Assert.EnterMultipleScope())
{
Assert.That(viewModel.ShowServiceControlQueueAddressEntry, Is.True,
"The entry field must be shown so the user can supply the address of a remote error instance");
Assert.That(viewModel.ShowServiceControlQueueAddressSelection, Is.False);
Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty,
"A validation error is expected until the user enters the error instance's queue address");
}

// User types the address of the error instance hosted on another machine
viewModel.ServiceControlQueueAddress = "Particular.ServiceControl.OnAnotherVM";

using (Assert.EnterMultipleScope())
{
Assert.That(viewModel.ServiceControlQueueAddress, Is.EqualTo("Particular.ServiceControl.OnAnotherVM"));
Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty);
}
}

[Test]
public void The_one_where_the_entered_address_contains_whitespace_and_is_rejected()
{
var viewModel = new ServiceControlAddViewModel
{
InstallErrorInstance = false,
InstallAuditInstance = true,
SubmitAttempted = true,
GetInstalledErrorInstanceNames = () => new string[0]
};

viewModel.ServiceControlQueueAddress = "Particular ServiceControl";

var notifyErrorInfo = GetNotifyErrorInfo(viewModel);

Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Not.Empty,
"Instance names never contain whitespace, so a queue address with whitespace must be rejected");
}

[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);

using (Assert.EnterMultipleScope())
{
Assert.That(viewModel.ShowServiceControlQueueAddressEntry, Is.False);
Assert.That(notifyErrorInfo.GetErrors(nameof(viewModel.ServiceControlQueueAddress)), Is.Empty);
}
}
}

static INotifyDataErrorInfo GetNotifyErrorInfo(object vm) => vm as INotifyDataErrorInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,24 @@

<controls:FormTextBox Header="NAME / WINDOWS SERVICE NAME" Text="{Binding AuditInstanceName}" />

<controls:FormComboBox HorizontalAlignment="Stretch"
Header="ERROR INSTANCE"
ItemsSource="{Binding ServiceControlQueueAddressOptions}"
SelectedItem="{Binding ServiceControlQueueAddress}"
Visibility="{Binding ShowServiceControlQueueAddressSelection,
Converter={StaticResource boolToVis}}" />

<StackPanel Visibility="{Binding ShowServiceControlQueueAddressEntry,
Converter={StaticResource boolToVis}}">
<controls:FormTextBox Header="ERROR INSTANCE QUEUE ADDRESS"
Text="{Binding ServiceControlQueueAddress, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Padding="0,0,0,10"
FontSize="13px"
Foreground="{StaticResource Gray60Brush}"
Text="No error instance was found on this machine. Enter the queue address of the error instance the audit instance sends messages to, e.g. one hosted on another machine."
TextWrapping="Wrap" />
</StackPanel>

<GroupBox Header="USER ACCOUNT">
<StackPanel>
<RadioButton IsChecked="{Binding AuditUseSystemAccount}">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace ServiceControl.Config.UI.InstanceAdd
namespace ServiceControl.Config.UI.InstanceAdd
{
using System;
using System.Collections.Generic;
Expand All @@ -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<string[]> 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();

Expand All @@ -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(ShowServiceControlQueueAddressEntry));
}
};
}

void ServiceControl_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
Expand All @@ -47,6 +64,40 @@ void ServiceControl_PropertyChanged(object sender, System.ComponentModel.Propert

public Func<string[]> GetWindowsServiceNames { get; set; }

public Func<string[]> 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 ShowServiceControlQueueAddressEntry =>
InstallAuditInstance
&& !InstallErrorInstance
&& GetInstalledErrorInstanceNames().Length == 0;

public string ConventionName { get; set; }

public void OnConventionNameChanged()
Expand Down
Loading
Loading