-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8c16b4a
commit 055a1fe
Showing
6 changed files
with
180 additions
and
57 deletions.
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
src/ProjectOrigin.Chronicler.Server/IRegistryClientFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
namespace ProjectOrigin.Chronicler.Server; | ||
|
||
public interface IRegistryClientFactory | ||
{ | ||
Registry.V1.RegistryService.RegistryServiceClient GetClient(string registryName); | ||
} |
62 changes: 62 additions & 0 deletions
62
src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System.Collections.Concurrent; | ||
using System.Net.Http; | ||
using Grpc.Net.Client; | ||
using Microsoft.Extensions.Options; | ||
using ProjectOrigin.Chronicler.Server.Exceptions; | ||
using ProjectOrigin.Chronicler.Server.Options; | ||
using static ProjectOrigin.Registry.V1.RegistryService; | ||
|
||
namespace ProjectOrigin.Chronicler.Server; | ||
|
||
public class RegistryClientFactory : IRegistryClientFactory | ||
{ | ||
private readonly ConcurrentDictionary<string, GrpcChannel> _registries = new ConcurrentDictionary<string, GrpcChannel>(); | ||
private readonly IOptionsMonitor<NetworkOptions> _optionsMonitor; | ||
|
||
public RegistryClientFactory(IOptionsMonitor<NetworkOptions> optionsMonitor) | ||
Check warning on line 16 in src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs GitHub Actions / analyse / sonar-analysis
Check warning on line 16 in src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs GitHub Actions / analyse / sonar-analysis
|
||
{ | ||
_optionsMonitor = optionsMonitor; | ||
_optionsMonitor.OnChange((options, _) => | ||
{ | ||
foreach (var record in _registries) | ||
{ | ||
if (options.RegistryUrls.TryGetValue(record.Key, out var uri)) | ||
{ | ||
if (record.Value.Target != uri) | ||
{ | ||
if (_registries.TryRemove(record.Key, out var channel)) | ||
channel.ShutdownAsync().Wait(); | ||
} | ||
else | ||
continue; | ||
Check warning on line 31 in src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs GitHub Actions / analyse / sonar-analysis
|
||
} | ||
else | ||
{ | ||
if (_registries.TryRemove(record.Key, out var channel)) | ||
channel.ShutdownAsync().Wait(); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
public RegistryServiceClient GetClient(string registryName) | ||
{ | ||
var channel = _registries.GetOrAdd( | ||
registryName, | ||
(name) => | ||
{ | ||
if (!_optionsMonitor.CurrentValue.RegistryUrls.TryGetValue(name, out var address)) | ||
throw new RegistryNotKnownException($"Registry {name} not found in configuration"); | ||
|
||
return GrpcChannel.ForAddress(address, new GrpcChannelOptions | ||
{ | ||
HttpHandler = new SocketsHttpHandler | ||
{ | ||
EnableMultipleHttp2Connections = true, | ||
} | ||
}); | ||
}); | ||
|
||
return new RegistryServiceClient(channel); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
src/ProjectOrigin.Chronicler.Test/RegistryServiceTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
using Moq; | ||
using ProjectOrigin.Chronicler.Server; | ||
using Grpc.Core; | ||
using FluentAssertions; | ||
|
||
namespace ProjectOrigin.Chronicler.Test; | ||
|
||
public class RegistryServiceTests | ||
{ | ||
private const string RegistryName = "someRegistry"; | ||
private const string GridArea = "Narnia"; | ||
private readonly Mock<IRegistryClientFactory> _factory; | ||
|
||
public RegistryServiceTests() | ||
{ | ||
_factory = new Mock<IRegistryClientFactory>(); | ||
} | ||
|
||
[Fact] | ||
public async Task GetNextBlock_EmptyResponse_ReturnsNull() | ||
{ | ||
// Arrange | ||
var registryService = new RegistryService(_factory.Object); | ||
var mockClient = new Mock<Registry.V1.RegistryService.RegistryServiceClient>(); | ||
mockClient.Setup(x => x.GetBlocksAsync(It.IsAny<Registry.V1.GetBlocksRequest>(), null, null, default)) | ||
.Returns(CreateAsyncUnaryCall(new Registry.V1.GetBlocksResponse | ||
{ | ||
Blocks = { } | ||
})); | ||
_factory.Setup(x => x.GetClient(RegistryName)).Returns(mockClient.Object); | ||
|
||
// Act | ||
var result = await registryService.GetNextBlock(RegistryName, 0); | ||
|
||
// Assert | ||
_factory.Verify(x => x.GetClient(RegistryName), Times.Once); | ||
result.Should().BeNull(); | ||
} | ||
|
||
[Theory] | ||
[InlineData(3)] | ||
[InlineData(6)] | ||
[InlineData(8)] | ||
public async Task GetNextBlock_ContainsSingle_ReturnsSingle(int height) | ||
{ | ||
// Arrange | ||
var registryService = new RegistryService(_factory.Object); | ||
var mockClient = new Mock<Registry.V1.RegistryService.RegistryServiceClient>(); | ||
mockClient.Setup(x => x.GetBlocksAsync(It.IsAny<Registry.V1.GetBlocksRequest>(), null, null, default)) | ||
.Returns(CreateAsyncUnaryCall(new Registry.V1.GetBlocksResponse | ||
{ | ||
Blocks = { | ||
new Registry.V1.Block { Height = height+1 } | ||
} | ||
})); | ||
_factory.Setup(x => x.GetClient(RegistryName)).Returns(mockClient.Object); | ||
|
||
// Act | ||
var result = await registryService.GetNextBlock(RegistryName, height); | ||
|
||
// Assert | ||
_factory.Verify(x => x.GetClient(RegistryName), Times.Once); | ||
mockClient.Verify(x => x.GetBlocksAsync(It.Is<Registry.V1.GetBlocksRequest>( | ||
x => x.Skip == height | ||
&& x.Limit == 1 | ||
&& x.IncludeTransactions == true | ||
), null, null, default), Times.Once); | ||
mockClient.VerifyNoOtherCalls(); | ||
result.Should().NotBeNull(); | ||
result!.Height.Should().Be(height + 1); | ||
} | ||
|
||
[Fact] | ||
public async Task GetNextBlock_ContainsMultiple_RaiseError() | ||
{ | ||
// Arrange | ||
var registryService = new RegistryService(_factory.Object); | ||
var mockClient = new Mock<Registry.V1.RegistryService.RegistryServiceClient>(); | ||
mockClient.Setup(x => x.GetBlocksAsync(It.IsAny<Registry.V1.GetBlocksRequest>(), null, null, default)) | ||
.Returns(CreateAsyncUnaryCall(new Registry.V1.GetBlocksResponse | ||
{ | ||
Blocks = { | ||
new Registry.V1.Block { Height = 0 }, | ||
new Registry.V1.Block { Height = 1 } | ||
} | ||
})); | ||
_factory.Setup(x => x.GetClient(RegistryName)).Returns(mockClient.Object); | ||
|
||
// Assert | ||
await Assert.ThrowsAsync<System.InvalidOperationException>(() => registryService.GetNextBlock(RegistryName, 0)); | ||
} | ||
|
||
private static AsyncUnaryCall<TResponse> CreateAsyncUnaryCall<TResponse>(TResponse response) | ||
{ | ||
return new AsyncUnaryCall<TResponse>( | ||
Task.FromResult(response), | ||
Task.FromResult(new Metadata()), | ||
() => Status.DefaultSuccess, | ||
() => new Metadata(), | ||
() => { }); | ||
} | ||
|
||
} |