Skip to content

Commit

Permalink
Implemented tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinSchmidt committed Aug 1, 2024
1 parent 8c16b4a commit 055a1fe
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 57 deletions.
6 changes: 6 additions & 0 deletions src/ProjectOrigin.Chronicler.Server/IRegistryClientFactory.cs
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 src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs
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

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Refactor this constructor to reduce its Cognitive Complexity from 20 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 16 in src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Refactor this constructor to reduce its Cognitive Complexity from 20 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
_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

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Remove this redundant jump. (https://rules.sonarsource.com/csharp/RSPEC-3626)

Check warning on line 31 in src/ProjectOrigin.Chronicler.Server/RegistryClientFactory.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Remove this redundant jump. (https://rules.sonarsource.com/csharp/RSPEC-3626)
}
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);
}
}
60 changes: 5 additions & 55 deletions src/ProjectOrigin.Chronicler.Server/RegistryService.cs
Original file line number Diff line number Diff line change
@@ -1,71 +1,21 @@

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
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 RegistryService : IRegistryService
{
private readonly ConcurrentDictionary<string, GrpcChannel> _registries = new ConcurrentDictionary<string, GrpcChannel>();
private readonly IOptionsMonitor<NetworkOptions> _optionsMonitor;

public RegistryService(IOptionsMonitor<NetworkOptions> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_optionsMonitor.OnChange((options, _) =>
{
foreach (var record in _registries)
{

if (options.RegistryUrls.TryGetValue(record.Key, out var uri))
{
if (record.Value.Target != uri)
{
_registries.TryRemove(record.Key, out var _);
record.Value.ShutdownAsync().Wait();
}
}
else
{
_registries.TryRemove(record.Key, out var _);
record.Value.ShutdownAsync().Wait();
}
}
});
}
private readonly IRegistryClientFactory _registryClientFactory;

private RegistryServiceClient CreateClient(string registryName)
public RegistryService(IRegistryClientFactory registryClientFactory)
{
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);
_registryClientFactory = registryClientFactory;
}

public async Task<Registry.V1.Block?> GetNextBlock(string registryName, int previousBlockHeight)
{
var client = CreateClient(registryName);
var client = _registryClientFactory.GetClient(registryName);

var response = await client.GetBlocksAsync(new Registry.V1.GetBlocksRequest
{
Skip = previousBlockHeight,
Expand Down
3 changes: 2 additions & 1 deletion src/ProjectOrigin.Chronicler.Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public void ConfigureServices(IServiceCollection services)
options.AddRepository<IChroniclerRepository, ChroniclerRepository>();
});

services.AddSingleton<IRegistryService, RegistryService>();
services.AddSingleton<IRegistryClientFactory, RegistryClientFactory>();
services.AddTransient<IRegistryService, RegistryService>();

services.AddOptions<ChroniclerOptions>()
.BindConfiguration(ChroniclerOptions.SectionPrefix)
Expand Down
1 change: 0 additions & 1 deletion src/ProjectOrigin.Chronicler.Test/BlockReaderJobTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using ProjectOrigin.Chronicler.Server.Models;
using AutoFixture;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;

namespace ProjectOrigin.Chronicler.Test;

Expand Down
105 changes: 105 additions & 0 deletions src/ProjectOrigin.Chronicler.Test/RegistryServiceTests.cs
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(),
() => { });
}

}

0 comments on commit 055a1fe

Please sign in to comment.