Skip to content

Commit 3468421

Browse files
committed
Extend unit tests for SAF.Hosting
1 parent fc36a00 commit 3468421

11 files changed

+336
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2017-2024 TRUMPF Laser GmbH
3+
4+
SPDX-License-Identifier: MPL-2.0
5+
-->
6+
7+
<Project Sdk="Microsoft.NET.Sdk">
8+
9+
<PropertyGroup>
10+
<TargetFramework>net8.0</TargetFramework>
11+
<ImplicitUsings>enable</ImplicitUsings>
12+
<Nullable>enable</Nullable>
13+
14+
<IsPackable>false</IsPackable>
15+
<IsTestProject>true</IsTestProject>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="coverlet.collector" />
20+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
21+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
22+
<PackageReference Include="xunit" />
23+
<PackageReference Include="xunit.runner.visualstudio" />
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<ProjectReference Include="..\SAF.Hosting.Contracts\SAF.Hosting.Contracts.csproj" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<Using Include="Xunit" />
32+
</ItemGroup>
33+
34+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-FileCopyrightText: 2017-2024 TRUMPF Laser GmbH
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
namespace SAF.Hosting.Contracts.Tests;
6+
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Xunit;
9+
10+
public class ServiceCollectionExtensionsTests
11+
{
12+
[Fact]
13+
[Obsolete("Remove this test when IHostedService gets removed.")]
14+
public void AddHostedAddsServiceOk()
15+
{
16+
var services = new ServiceCollection();
17+
services.AddHosted<MockHostedService>();
18+
19+
Assert.Contains(services, sd => sd.ServiceType == typeof(IHostedService));
20+
Assert.Contains(services, sd => sd.ImplementationType == typeof(MockHostedService));
21+
}
22+
23+
[Fact]
24+
public void AddHostedAsyncAddsServiceOk()
25+
{
26+
var services = new ServiceCollection();
27+
services.AddHostedAsync<MockHostedServiceAsync>();
28+
29+
Assert.Contains(services, sd => sd.ServiceType == typeof(IHostedServiceAsync));
30+
Assert.Contains(services, sd => sd.ImplementationType == typeof(MockHostedServiceAsync));
31+
}
32+
33+
[Obsolete("Remove this class when IHostedService gets removed.")]
34+
private class MockHostedService : IHostedService
35+
{
36+
public void Start() { }
37+
public void Stop() { }
38+
public void Kill() { }
39+
}
40+
41+
private class MockHostedServiceAsync : IHostedServiceAsync
42+
{
43+
public Task StartAsync(CancellationToken cancelToken) => Task.CompletedTask;
44+
public Task StopAsync(CancellationToken cancelToken) => Task.CompletedTask;
45+
}
46+
}

src/Hosting/SAF.Hosting.Contracts/SAF.Hosting.Contracts.csproj

+5
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ SPDX-License-Identifier: MPL-2.0
1717
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
1818
</ItemGroup>
1919

20+
<ItemGroup>
21+
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
22+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
23+
</ItemGroup>
24+
2025
</Project>

src/Hosting/SAF.Hosting.Tests/ServiceHostBuilderExtensionsTests.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ public void AddServiceAssemblySearchAddsRequiredServices()
3030
Assert.Contains(services, sd => sd.ServiceType == typeof(IPostConfigureOptions<ServiceAssemblySearchOptions>));
3131
}
3232

33-
[Fact]
34-
public void ServiceAssemblySearchOptionsGetsValidatedOnServiceRequest()
33+
[Theory]
34+
[InlineData("", "pattern", "searchPath")]
35+
[InlineData("basePath", "", "searchPath")]
36+
[InlineData("basePath", "pattern", "")]
37+
public void ServiceAssemblySearchOptionsGetsValidatedOnServiceRequest(string basePath, string filenamePattern, string searchPath)
3538
{
3639
// Arrange
3740
var services = new ServiceCollection();
@@ -40,11 +43,23 @@ public void ServiceAssemblySearchOptionsGetsValidatedOnServiceRequest()
4043
var builder = new ServiceHostBuilder(services);
4144

4245
// Act
43-
builder.AddServiceAssemblySearch(options => options.BasePath = string.Empty);
46+
builder.AddServiceAssemblySearch(options =>
47+
{
48+
options.BasePath = basePath;
49+
options.SearchFilenamePattern = filenamePattern;
50+
options.SearchPath = searchPath;
51+
});
4452
var sp = builder.Services.BuildServiceProvider();
4553

4654
// Assert
47-
Assert.Throws<InvalidOperationException>(() => _ = sp.GetService<IServiceAssemblySearch>());
55+
var ex = Assert.Throws<InvalidOperationException>(() => _ = sp.GetService<IServiceAssemblySearch>());
56+
57+
if(string.IsNullOrEmpty(basePath))
58+
Assert.Contains("BasePath", ex.Message);
59+
if (string.IsNullOrEmpty(filenamePattern))
60+
Assert.Contains("SearchFilenamePattern", ex.Message);
61+
if (string.IsNullOrEmpty(searchPath))
62+
Assert.Contains("SearchPath", ex.Message);
4863
}
4964

5065
[Fact]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-FileCopyrightText: 2017-2024 TRUMPF Laser GmbH
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
namespace SAF.Hosting.Tests;
6+
7+
using Contracts;
8+
using NSubstitute;
9+
using Xunit;
10+
11+
public class ServiceHostInfoTests
12+
{
13+
[Fact]
14+
public void IdReturnsInitializedIdWhenIdInOptionsIsNotNull()
15+
{
16+
// Arrange
17+
var options = new ServiceHostInfoOptions { Id = "test-id" };
18+
var initializeId = Substitute.For<Func<string>>();
19+
var serviceHostInfo = new ServiceHostInfo(options, initializeId);
20+
21+
// Act
22+
var id = serviceHostInfo.Id;
23+
24+
// Assert
25+
Assert.Equal("test-id", id);
26+
initializeId.DidNotReceive().Invoke();
27+
}
28+
29+
[Fact]
30+
public void IdInvokesInitializeIdWhenIdInOptionsIsNull()
31+
{
32+
// Arrange
33+
var options = new ServiceHostInfoOptions { Id = null };
34+
var initializeId = Substitute.For<Func<string>>();
35+
initializeId.Invoke().Returns("initialized-id");
36+
var serviceHostInfo = new ServiceHostInfo(options, initializeId);
37+
38+
// Act
39+
var id = serviceHostInfo.Id;
40+
41+
// Assert
42+
Assert.Equal("initialized-id", id);
43+
initializeId.Received(1).Invoke();
44+
}
45+
46+
[Fact]
47+
public void ServiceHostTypeReturnsServiceHostTypeFromOptions()
48+
{
49+
// Arrange
50+
var options = new ServiceHostInfoOptions { ServiceHostType = "test-type" };
51+
var initializeId = Substitute.For<Func<string>>();
52+
var serviceHostInfo = new ServiceHostInfo(options, initializeId);
53+
54+
// Act
55+
var serviceHostType = serviceHostInfo.ServiceHostType;
56+
57+
// Assert
58+
Assert.Equal("test-type", serviceHostType);
59+
}
60+
61+
[Fact]
62+
public void FileSystemUserBasePathReturnsFileSystemUserBasePathFromOptions()
63+
{
64+
// Arrange
65+
var options = new ServiceHostInfoOptions { FileSystemUserBasePath = "user-base-path" };
66+
var initializeId = Substitute.For<Func<string>>();
67+
var serviceHostInfo = new ServiceHostInfo(options, initializeId);
68+
69+
// Act
70+
var fileSystemUserBasePath = serviceHostInfo.FileSystemUserBasePath;
71+
72+
// Assert
73+
Assert.Equal("user-base-path", fileSystemUserBasePath);
74+
}
75+
76+
[Fact]
77+
public void FileSystemInstallationPathReturnsFileSystemInstallationPathFromOptions()
78+
{
79+
// Arrange
80+
var options = new ServiceHostInfoOptions { FileSystemInstallationPath = "installation-path" };
81+
var initializeId = Substitute.For<Func<string>>();
82+
var serviceHostInfo = new ServiceHostInfo(options, initializeId);
83+
84+
// Act
85+
var fileSystemInstallationPath = serviceHostInfo.FileSystemInstallationPath;
86+
87+
// Assert
88+
Assert.Equal("installation-path", fileSystemInstallationPath);
89+
}
90+
91+
[Fact]
92+
public void UpSinceReturnsCurrentDateTimeOffset()
93+
{
94+
// Arrange
95+
var options = new ServiceHostInfoOptions();
96+
var initializeId = Substitute.For<Func<string>>();
97+
var serviceHostInfo = new ServiceHostInfo(options, initializeId);
98+
99+
// Act
100+
var upSince = serviceHostInfo.UpSince;
101+
102+
// Assert
103+
Assert.True((DateTimeOffset.Now - upSince).TotalSeconds < 1);
104+
}
105+
}

src/Hosting/SAF.Hosting.Tests/ServiceMessageDispatcherTests.cs

+47-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
namespace SAF.Hosting.Tests;
66

77
using Common;
8+
using Microsoft.Extensions.Logging;
89
using Microsoft.Extensions.Logging.Abstractions;
910
using Microsoft.VisualBasic;
1011
using NSubstitute;
12+
using TestUtilities;
1113
using Xunit;
1214

1315
public class ServiceMessageDispatcherTests
@@ -56,7 +58,29 @@ public void DispatchCatchesExceptionOccuringInHandle()
5658
// Arrange
5759
var handler = Substitute.For<DummyMessageHandler>();
5860
handler.CanHandle(Arg.Any<Message>()).Returns(true);
59-
handler.When(h => h.Handle(Arg.Any<Message>())).Do(_ => throw new Exception());
61+
handler.When(h => h.Handle(Arg.Any<Message>()))
62+
.Do(_ => throw new Exception());
63+
64+
var dispatcher = new ServiceMessageDispatcher(NullLogger<ServiceMessageDispatcher>.Instance);
65+
var testMessage = new Message();
66+
67+
// Act
68+
dispatcher.AddHandler<DummyMessageHandler>(() => handler);
69+
dispatcher.DispatchMessage<DummyMessageHandler>(testMessage);
70+
71+
// Assert
72+
handler.Received(1).CanHandle(Arg.Any<Message>());
73+
handler.Received(1).Handle(Arg.Any<Message>());
74+
}
75+
76+
[Fact]
77+
public void DispatchCatchesObjectDisposedExceptionOccuringInHandle()
78+
{
79+
// Arrange
80+
var handler = Substitute.For<DummyMessageHandler>();
81+
handler.CanHandle(Arg.Any<Message>()).Returns(true);
82+
handler.When(h => h.Handle(Arg.Any<Message>()))
83+
.Do(_ => throw new ObjectDisposedException("test"));
6084

6185
var dispatcher = new ServiceMessageDispatcher(NullLogger<ServiceMessageDispatcher>.Instance);
6286
var testMessage = new Message();
@@ -108,6 +132,28 @@ public void DispatchCatchesExceptionOccuringInAction()
108132
Assert.Equal(testMessage, actionMessage);
109133
}
110134

135+
[Fact]
136+
public void DispatchLogsErrorWhenMessageHandlerIsUnknown()
137+
{
138+
// Arrange
139+
var handler = Substitute.For<DummyMessageHandler>();
140+
handler.CanHandle(Arg.Any<Message>()).Returns(true);
141+
142+
var mockLogger = Substitute.For<MockLogger<ServiceMessageDispatcher>>();
143+
144+
var dispatcher = new ServiceMessageDispatcher(mockLogger);
145+
var testMessage = new Message();
146+
147+
// Act
148+
dispatcher.AddHandler<DummyMessageHandler>(() => handler);
149+
dispatcher.DispatchMessage(typeof(DateTimeOffset), testMessage);
150+
151+
// Assert
152+
mockLogger.Received(1).Log(LogLevel.Error, "Handler {handlerTypeFullName} unknown!", typeof(DateTimeOffset).FullName);
153+
handler.DidNotReceive().CanHandle(Arg.Any<Message>());
154+
handler.DidNotReceive().Handle(Arg.Any<Message>());
155+
}
156+
111157
public abstract class DummyMessageHandler : IMessageHandler
112158
{
113159
public virtual bool CanHandle(Message message) => throw new NotImplementedException();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-FileCopyrightText: 2017-2024 TRUMPF Laser GmbH
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
namespace SAF.Hosting.Tests;
6+
7+
using Contracts;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using NSubstitute;
10+
using Xunit;
11+
12+
public class SharedServicesRegistryExtensionsTests
13+
{
14+
private readonly ISharedServiceRegistry _registry = new SharedServiceRegistry();
15+
16+
[Fact]
17+
public void RedirectServicesRegistersSingletonService()
18+
{
19+
// Arrange
20+
var testService = Substitute.For<IDummyService>();
21+
_registry.Services.AddSingleton(_ => testService);
22+
using var sourceProvider = _registry.Services.BuildServiceProvider();
23+
24+
var target = new ServiceCollection();
25+
26+
// Act
27+
_registry.RedirectServices(sourceProvider, target);
28+
29+
// Assert
30+
using var serviceProvider = target.BuildServiceProvider();
31+
var resolvedService = serviceProvider.GetRequiredService<IDummyService>();
32+
Assert.Same(testService, resolvedService);
33+
}
34+
35+
[Fact]
36+
public void RedirectServicesRegistersTransientService()
37+
{
38+
// Arrange
39+
_registry.Services.AddTransient(_ => Substitute.For<IDummyService>());
40+
using var sourceProvider = _registry.Services.BuildServiceProvider();
41+
42+
var target = new ServiceCollection();
43+
44+
// Act
45+
_registry.RedirectServices(sourceProvider, target);
46+
47+
// Assert
48+
using var serviceProvider = target.BuildServiceProvider();
49+
50+
var resolvedService1 = serviceProvider.GetRequiredService<IDummyService>();
51+
var resolvedService2 = serviceProvider.GetRequiredService<IDummyService>();
52+
Assert.NotSame(resolvedService1, resolvedService2);
53+
}
54+
55+
[Fact]
56+
public void RedirectServicesThrowsInvalidOperationExceptionForScopedServices()
57+
{
58+
// Arrange
59+
_registry.Services.AddScoped(_ => Substitute.For<IDummyService>());
60+
using var sourceProvider = _registry.Services.BuildServiceProvider();
61+
62+
var target = new ServiceCollection();
63+
64+
// Act & Assert
65+
var ex = Assert.Throws<InvalidOperationException>(() => _registry.RedirectServices(sourceProvider, target));
66+
Assert.Contains("Scoped service is not supported", ex.Message);
67+
}
68+
69+
public interface IDummyService;
70+
}

src/Hosting/SAF.Hosting/ServiceAssemblySearchOptions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace SAF.Hosting;
66

77
/// <summary>
8-
/// Provides options to be used for the <see cref="Abstractions.IServiceAssemblySearch"/>.
8+
/// Provides options to be used for the <see cref="Contracts.IServiceAssemblySearch"/>.
99
/// </summary>
1010
public class ServiceAssemblySearchOptions
1111
{

0 commit comments

Comments
 (0)