diff --git a/src/ProjectOrigin.Stamp.Server/Database/IUnitOfWork.cs b/src/ProjectOrigin.Stamp.Server/Database/IUnitOfWork.cs new file mode 100644 index 0000000..1315307 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Database/IUnitOfWork.cs @@ -0,0 +1,11 @@ +using ProjectOrigin.Stamp.Server.Repositories; + +namespace ProjectOrigin.Stamp.Server.Database; + +public interface IUnitOfWork +{ + void Commit(); + void Rollback(); + + IRecipientRepository RecipientRepository { get; } +} diff --git a/src/ProjectOrigin.Stamp.Server/Database/Postgres/PostgresConnectionFactory.cs b/src/ProjectOrigin.Stamp.Server/Database/Postgres/PostgresConnectionFactory.cs index 3f887d8..61678c9 100644 --- a/src/ProjectOrigin.Stamp.Server/Database/Postgres/PostgresConnectionFactory.cs +++ b/src/ProjectOrigin.Stamp.Server/Database/Postgres/PostgresConnectionFactory.cs @@ -1,7 +1,6 @@ using System.Data; using Microsoft.Extensions.Options; using Npgsql; -using ProjectOrigin.Stamp.Server.Database.Postgres; namespace ProjectOrigin.Stamp.Server.Database.Postgres; diff --git a/src/ProjectOrigin.Stamp.Server/Database/Postgres/Scripts/0001.sql b/src/ProjectOrigin.Stamp.Server/Database/Postgres/Scripts/0001.sql new file mode 100644 index 0000000..b5abbd7 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Database/Postgres/Scripts/0001.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS Recipients ( + id uuid NOT NULL PRIMARY KEY, + wallet_endpoint_reference_version integer NOT NULL, + wallet_endpoint_reference_endpoint VARCHAR(512) NOT NULL, + wallet_endpoint_reference_public_key bytea NOT NULL +); diff --git a/src/ProjectOrigin.Stamp.Server/Database/Postgres/Scripts/1.sql b/src/ProjectOrigin.Stamp.Server/Database/Postgres/Scripts/1.sql deleted file mode 100644 index e69de29..0000000 diff --git a/src/ProjectOrigin.Stamp.Server/Database/UnitOfWork.cs b/src/ProjectOrigin.Stamp.Server/Database/UnitOfWork.cs new file mode 100644 index 0000000..cdca548 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Database/UnitOfWork.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Data; +using ProjectOrigin.Stamp.Server.Repositories; + +namespace ProjectOrigin.Stamp.Server.Database; + +public class UnitOfWork : IUnitOfWork, IDisposable +{ + private IRecipientRepository _recipientRepository = null!; + + public IRecipientRepository RecipientRepository + { + get + { + return _recipientRepository ??= new RecipientRepository(_lazyTransaction.Value.Connection ?? throw new InvalidOperationException("Transaction is null.")); + } + } + + private readonly Dictionary _repositories = new Dictionary(); + private readonly Lazy _lazyConnection; + private Lazy _lazyTransaction; + private bool _disposed = false; + + public UnitOfWork(IDbConnectionFactory connectionFactory) + { + _lazyConnection = new Lazy(() => + { + var connection = connectionFactory.CreateConnection(); + connection.Open(); + return connection; + }); + + _lazyTransaction = new Lazy(_lazyConnection.Value.BeginTransaction); + } + + public void Commit() + { + if (!_lazyTransaction.IsValueCreated) + return; + + try + { + _lazyTransaction.Value.Commit(); + } + catch + { + _lazyTransaction.Value.Rollback(); + throw; + } + finally + { + ResetUnitOfWork(); + } + } + + public void Rollback() + { + if (!_lazyTransaction.IsValueCreated) + return; + + _lazyTransaction.Value.Rollback(); + + ResetUnitOfWork(); + } + + private void ResetUnitOfWork() + { + if (_lazyTransaction.IsValueCreated) + _lazyTransaction.Value.Dispose(); + + _lazyTransaction = new Lazy(_lazyConnection.Value.BeginTransaction); + + _repositories.Clear(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~UnitOfWork() => Dispose(false); + + protected virtual void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + if (_lazyTransaction.IsValueCreated) + { + _lazyTransaction.Value.Dispose(); + } + + if (_lazyConnection.IsValueCreated) + { + _lazyConnection.Value.Dispose(); + } + } + + _repositories.Clear(); + } +} diff --git a/src/ProjectOrigin.Stamp.Server/Models/Recipient.cs b/src/ProjectOrigin.Stamp.Server/Models/Recipient.cs new file mode 100644 index 0000000..71f5e52 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Models/Recipient.cs @@ -0,0 +1,12 @@ +using System; +using ProjectOrigin.HierarchicalDeterministicKeys.Interfaces; + +namespace ProjectOrigin.Stamp.Server.Models; + +public class Recipient +{ + public required Guid Id { get; init; } + public required int WalletEndpointReferenceVersion { get; init; } + public required string WalletEndpointReferenceEndpoint { get; init; } + public required IHDPublicKey WalletEndpointReferencePublicKey { get; init; } +} diff --git a/src/ProjectOrigin.Stamp.Server/Properties/launchSettings.json b/src/ProjectOrigin.Stamp.Server/Properties/launchSettings.json new file mode 100644 index 0000000..16a5741 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ProjectOrigin.Stamp.Server": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57881;http://localhost:57882" + } + } +} \ No newline at end of file diff --git a/src/ProjectOrigin.Stamp.Server/Repositories/RecipientRepository.cs b/src/ProjectOrigin.Stamp.Server/Repositories/RecipientRepository.cs new file mode 100644 index 0000000..225ec30 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Repositories/RecipientRepository.cs @@ -0,0 +1,46 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using Dapper; +using ProjectOrigin.Stamp.Server.Models; + +namespace ProjectOrigin.Stamp.Server.Repositories; + +public interface IRecipientRepository +{ + Task Create(Recipient recipient); + Task Get(Guid id); +} + +public class RecipientRepository : IRecipientRepository +{ + private readonly IDbConnection _connection; + + public RecipientRepository(IDbConnection connection) + { + _connection = connection; + } + + public Task Create(Recipient recipient) + { + return _connection.ExecuteAsync( + @"INSERT INTO recipients (id, wallet_endpoint_reference_version, wallet_endpoint_reference_endpoint, wallet_endpoint_reference_public_key) + VALUES (@Id, @WalletEndpointReferenceVersion, @WalletEndpointReferenceEndpoint, @WalletEndpointReferencePublicKey)", + new + { + recipient.Id, + recipient.WalletEndpointReferenceVersion, + recipient.WalletEndpointReferenceEndpoint, + recipient.WalletEndpointReferencePublicKey + }); + } + + public Task Get(Guid id) + { + return _connection.QueryFirstOrDefaultAsync( + @"SELECT id, wallet_endpoint_reference_version, wallet_endpoint_reference_endpoint, wallet_endpoint_reference_public_key + FROM recipients + WHERE id = @Id", + new { Id = id }); + } +} diff --git a/src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs b/src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs new file mode 100644 index 0000000..2673bf7 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using ProjectOrigin.Stamp.Server.Database; +using ProjectOrigin.Stamp.Server.Models; +using ProjectOrigin.HierarchicalDeterministicKeys.Implementations; +using Microsoft.AspNetCore.Http; + +namespace ProjectOrigin.Stamp.Server.Services.REST.v1; + +[ApiController] +public class RecipientController : ControllerBase +{ + /// + /// Creates a new recipient + /// + /// + /// The create recipient request + /// The recipient was created. + [HttpPost] + [Route("v1/recipients")] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task> CreateRecipient( + [FromServices] IUnitOfWork unitOfWork, + [FromBody] CreateRecipientRequest request) + { + var recipient = new Recipient + { + Id = Guid.NewGuid(), + WalletEndpointReferenceVersion = request.WalletEndpointReference.Version, + WalletEndpointReferenceEndpoint = request.WalletEndpointReference.Endpoint.ToString(), + WalletEndpointReferencePublicKey = new Secp256k1Algorithm().ImportHDPublicKey(request.WalletEndpointReference.PublicKey) + }; + + await unitOfWork.RecipientRepository.Create(recipient); + + unitOfWork.Commit(); + + return Created(null as string, new CreateRecipientResponse + { + Id = recipient.Id + }); + } +} + +#region Records + +/// +/// Request to create a new recipient. +/// +public record CreateRecipientRequest +{ + /// + /// The recipient wallet endpoint reference. + /// + public required WalletEndpointReferenceDto WalletEndpointReference { get; init; } +} + +public record WalletEndpointReferenceDto +{ + /// + /// The version of the ReceiveSlice API. + /// + public required int Version { get; init; } + + /// + /// The url endpoint of where the wallet is hosted. + /// + public required Uri Endpoint { get; init; } + + /// + /// The public key used to generate sub-public-keys for each slice. + /// + public required byte[] PublicKey { get; init; } +} + +/// +/// Response to create a recipient. +/// +public record CreateRecipientResponse +{ + /// + /// The ID of the created recipient. + /// + public required Guid Id { get; init; } +} +#endregion diff --git a/src/ProjectOrigin.Stamp.Server/Startup.cs b/src/ProjectOrigin.Stamp.Server/Startup.cs index 9a55294..180afc1 100644 --- a/src/ProjectOrigin.Stamp.Server/Startup.cs +++ b/src/ProjectOrigin.Stamp.Server/Startup.cs @@ -113,6 +113,7 @@ public void ConfigureServices(IServiceCollection services) o.ConfigureMassTransitTransport(_configuration.GetSection("MessageBroker").GetValid()); }); + services.AddScoped(); services.AddSingleton(); services.AddSwaggerGen(options => diff --git a/src/ProjectOrigin.Stamp.Test/Extensions/HttpClientExtensions.cs b/src/ProjectOrigin.Stamp.Test/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..041c65d --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/Extensions/HttpClientExtensions.cs @@ -0,0 +1,21 @@ +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ProjectOrigin.Stamp.Test.Extensions; + +public static class HttpClientExtensions +{ + public static async Task ReadJson(this HttpContent content) + { + var options = GetJsonSerializerOptions(); + return await content.ReadFromJsonAsync(options); + } + + private static JsonSerializerOptions GetJsonSerializerOptions() + { + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + options.Converters.Add(new JsonStringEnumConverter()); + return options; + } +} diff --git a/src/ProjectOrigin.Stamp.Test/ProjectOrigin.Stamp.Test.csproj b/src/ProjectOrigin.Stamp.Test/ProjectOrigin.Stamp.Test.csproj index fa71b7a..e4ca77a 100644 --- a/src/ProjectOrigin.Stamp.Test/ProjectOrigin.Stamp.Test.csproj +++ b/src/ProjectOrigin.Stamp.Test/ProjectOrigin.Stamp.Test.csproj @@ -6,4 +6,20 @@ enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/ProjectOrigin.Stamp.Test/REST/RecipientControllerTests.cs b/src/ProjectOrigin.Stamp.Test/REST/RecipientControllerTests.cs new file mode 100644 index 0000000..258b949 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/REST/RecipientControllerTests.cs @@ -0,0 +1,54 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using ProjectOrigin.HierarchicalDeterministicKeys.Implementations; +using ProjectOrigin.Stamp.Server; +using ProjectOrigin.Stamp.Server.Repositories; +using ProjectOrigin.Stamp.Server.Services.REST.v1; +using ProjectOrigin.Stamp.Test.Extensions; +using ProjectOrigin.Stamp.Test.TestClassFixtures; +using Xunit; + +namespace ProjectOrigin.Stamp.Test.REST; + +public class RecipientControllerTests : IClassFixture>, IClassFixture +{ + private readonly TestServerFixture _fixture; + private readonly PostgresDatabaseFixture _postgres; + + public RecipientControllerTests(TestServerFixture fixture, PostgresDatabaseFixture postgres) + { + fixture.PostgresConnectionString = postgres.ConnectionString; + _fixture = fixture; + _postgres = postgres; + } + + [Fact] + public async Task CreateAndGetRecipient() + { + using var client = _fixture.CreateHttpClient(); + + var createRecipientRequest = new CreateRecipientRequest + { + WalletEndpointReference = new WalletEndpointReferenceDto + { + Endpoint = new Uri("http://foo"), + PublicKey = new Secp256k1Algorithm().GenerateNewPrivateKey().Neuter().Export().ToArray(), + Version = 1 + } + }; + + var post = await client.PostAsJsonAsync("stamp-api/v1/recipients", createRecipientRequest); + post.StatusCode.Should().Be(HttpStatusCode.Created); + var response = await post.Content.ReadJson(); + + using var connection = _postgres.GetConnectionFactory().CreateConnection(); + connection.Open(); + var repo = new RecipientRepository(connection); + + var recipient = await repo.Get(response!.Id); + recipient!.WalletEndpointReferenceVersion.Should().Be(createRecipientRequest.WalletEndpointReference.Version); + recipient.WalletEndpointReferenceEndpoint.Should().Be(createRecipientRequest.WalletEndpointReference.Endpoint.ToString()); + recipient.WalletEndpointReferencePublicKey.Export().ToArray().Should().BeEquivalentTo(createRecipientRequest.WalletEndpointReference.PublicKey); + } +} diff --git a/src/ProjectOrigin.Stamp.Test/Repositories/RecipientRepositoryTests.cs b/src/ProjectOrigin.Stamp.Test/Repositories/RecipientRepositoryTests.cs new file mode 100644 index 0000000..111ad8c --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/Repositories/RecipientRepositoryTests.cs @@ -0,0 +1,45 @@ +using FluentAssertions; +using ProjectOrigin.Stamp.Server.Repositories; +using Npgsql; +using ProjectOrigin.HierarchicalDeterministicKeys.Implementations; +using ProjectOrigin.Stamp.Server.Models; +using ProjectOrigin.Stamp.Test.TestClassFixtures; +using Xunit; + +namespace ProjectOrigin.Stamp.Test.Repositories; + +public class RecipientRepositoryTests : IClassFixture +{ + private readonly RecipientRepository _repository; + + public RecipientRepositoryTests(PostgresDatabaseFixture dbFixture) + { + var connection = new NpgsqlConnection(dbFixture.ConnectionString); + connection.Open(); + _repository = new RecipientRepository(connection); + } + + [Fact] + public async Task CreateAndQueryRecipient() + { + var privateKey = new Secp256k1Algorithm().GenerateNewPrivateKey(); + var recipient = new Recipient + { + Id = Guid.NewGuid(), + WalletEndpointReferenceEndpoint = "http://foo", + WalletEndpointReferencePublicKey = privateKey.Neuter(), + WalletEndpointReferenceVersion = 1 + }; + + await _repository.Create(recipient); + + var queriedRecipient = await _repository.Get(recipient.Id); + + queriedRecipient.Should().NotBeNull(); + + queriedRecipient!.Id.Should().Be(recipient.Id); + queriedRecipient.WalletEndpointReferenceVersion.Should().Be(recipient.WalletEndpointReferenceVersion); + queriedRecipient.WalletEndpointReferenceEndpoint.Should().Be(recipient.WalletEndpointReferenceEndpoint); + queriedRecipient.WalletEndpointReferencePublicKey.Export().ToArray().Should().BeEquivalentTo(recipient.WalletEndpointReferencePublicKey.Export().ToArray()); + } +} diff --git a/src/ProjectOrigin.Stamp.Test/TestClassFixtures/PostgresDatabaseFixture.cs b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/PostgresDatabaseFixture.cs new file mode 100644 index 0000000..d93ed14 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/PostgresDatabaseFixture.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ProjectOrigin.HierarchicalDeterministicKeys.Implementations; +using ProjectOrigin.Stamp.Server.Database; +using ProjectOrigin.Stamp.Server.Database.Mapping; +using ProjectOrigin.Stamp.Server.Database.Postgres; +using Testcontainers.PostgreSql; +using Xunit; + +namespace ProjectOrigin.Stamp.Test.TestClassFixtures; + +public class PostgresDatabaseFixture : IAsyncLifetime +{ + public string ConnectionString => _postgreSqlContainer.GetConnectionString(); + + private PostgreSqlContainer _postgreSqlContainer; + + public PostgresDatabaseFixture() + { + _postgreSqlContainer = new PostgreSqlBuilder() + .WithImage("postgres:15") + .Build(); + + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + + var algorithm = new Secp256k1Algorithm(); + ApplicationBuilderExtension.ConfigureMappers(algorithm); + } + + public async Task InitializeAsync() + { + await _postgreSqlContainer.StartAsync(); + await UpgradeDatabase(); + } + + public async Task UpgradeDatabase() + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + + var upgrader = new PostgresUpgrader( + loggerFactory.CreateLogger(), + Options.Create(new PostgresOptions + { + ConnectionString = _postgreSqlContainer.GetConnectionString() + })); + + await upgrader.Upgrade(); + } + + public IDbConnectionFactory GetConnectionFactory() => new PostgresConnectionFactory(Options.Create(new PostgresOptions + { + ConnectionString = _postgreSqlContainer.GetConnectionString() + })); + + public Task DisposeAsync() + { + return _postgreSqlContainer.StopAsync(); + } +} diff --git a/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerFixture.cs b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerFixture.cs new file mode 100644 index 0000000..7086621 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerFixture.cs @@ -0,0 +1,161 @@ +#region Copyright notice and license +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/test-services/sample/Tests/Server/IntegrationTests/Helpers/GrpcTestFixture.cs +#endregion + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ProjectOrigin.Stamp.Test.TestClassFixtures.TestServerHelpers; +using Xunit.Abstractions; + +namespace ProjectOrigin.Stamp.Test.TestClassFixtures +{ + public delegate void LogMessage(LogLevel logLevel, string categoryName, EventId eventId, string message, Exception? exception); + + public class TestServerFixture : IDisposable where TStartup : class + { + private TestServer? _server; + private IHost? _host; + private HttpMessageHandler? _handler; + private Dictionary? _configurationDictionary; + private bool _disposed = false; + public event Action? ConfigureTestServices; + + public event LogMessage? LoggedMessage; + + public string PostgresConnectionString { get; set; } = "http://foo"; + + public TestServerFixture() + { + LoggerFactory = new LoggerFactory(); + LoggerFactory.AddProvider(new ForwardingLoggerProvider((logLevel, category, eventId, message, exception) => + { + LoggedMessage?.Invoke(logLevel, category, eventId, message, exception); + })); + } + + public T GetRequiredService() where T : class + { + EnsureServer(); + return _host!.Services.GetRequiredService(); + } + + public void ConfigureHostConfiguration(Dictionary configuration) + { + if (_configurationDictionary != null) + foreach (var keyValuePair in configuration) + { + _configurationDictionary[keyValuePair.Key] = keyValuePair.Value; + } + else + _configurationDictionary = configuration; + } + + private void EnsureServer() + { + if (_host == null) + { + ConfigureHostConfiguration(new Dictionary + { + {"Otlp:Enabled", "false"}, + {"RestApiOptions:PathBase", "/stamp-api"}, + {"MessageBroker:Type", "InMemory"}, + {"ConnectionStrings:Database", PostgresConnectionString} + }); + + var builder = new HostBuilder(); + + if (_configurationDictionary != null) + { + builder.ConfigureHostConfiguration(config => + { + config.Add(new MemoryConfigurationSource() + { + InitialData = _configurationDictionary + }); + }); + } + + builder + .ConfigureServices(services => + { + services.AddSingleton(LoggerFactory); + }) + .ConfigureWebHostDefaults(webHost => + { + webHost + .UseTestServer() + .UseEnvironment("Development") + .UseStartup(); + }) + .ConfigureServices(services => + { + if (ConfigureTestServices != null) + ConfigureTestServices.Invoke(services); + }); + + _host = builder.Start(); + _server = _host.GetTestServer(); + _handler = _server.CreateHandler(); + } + } + + public LoggerFactory LoggerFactory { get; } + + public HttpClient CreateHttpClient() + { + EnsureServer(); + + var client = _server!.CreateClient(); + return client; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _handler?.Dispose(); + _host?.Dispose(); + _server?.Dispose(); + } + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~TestServerFixture() + { + Dispose(false); + } + + public IDisposable GetTestLogger(ITestOutputHelper outputHelper) + { + return new TestServerContext(this, outputHelper); + } + } +} + diff --git a/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerHelpers/ForwardLoggingProvider.cs b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerHelpers/ForwardLoggingProvider.cs new file mode 100644 index 0000000..1f27455 --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerHelpers/ForwardLoggingProvider.cs @@ -0,0 +1,73 @@ +#region Copyright notice and license +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/test-services/sample/Tests/Server/IntegrationTests/Helpers/ForwardingLoggerProvider.cs +#endregion + +using Microsoft.Extensions.Logging; + +namespace ProjectOrigin.Stamp.Test.TestClassFixtures.TestServerHelpers +{ + internal class ForwardingLoggerProvider : ILoggerProvider + { + private readonly LogMessage _logAction; + + public ForwardingLoggerProvider(LogMessage logAction) + { + _logAction = logAction; + } + + public ILogger CreateLogger(string categoryName) + { + return new ForwardingLogger(categoryName, _logAction); + } + + public void Dispose() + { + } + + internal class ForwardingLogger : ILogger, IDisposable + { + private readonly string _categoryName; + private readonly LogMessage _logAction; + + public ForwardingLogger(string categoryName, LogMessage logAction) + { + _categoryName = categoryName; + _logAction = logAction; + } + + IDisposable ILogger.BeginScope(TState state) + { + return this; + } + + public void Dispose() + { + + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _logAction(logLevel, _categoryName, eventId, formatter(state, exception), exception); + } + } + } +} diff --git a/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerHelpers/TestServerContext.cs b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerHelpers/TestServerContext.cs new file mode 100644 index 0000000..215576e --- /dev/null +++ b/src/ProjectOrigin.Stamp.Test/TestClassFixtures/TestServerHelpers/TestServerContext.cs @@ -0,0 +1,54 @@ +#region Copyright notice and license +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/test-services/sample/Tests/Server/IntegrationTests/Helpers/GrpcTestContext.cs +#endregion + +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace ProjectOrigin.Stamp.Test.TestClassFixtures.TestServerHelpers +{ + internal class TestServerContext : IDisposable where TStartup : class + { + private readonly Stopwatch _stopwatch; + private readonly TestServerFixture _fixture; + private readonly ITestOutputHelper _outputHelper; + + public TestServerContext(TestServerFixture fixture, ITestOutputHelper outputHelper) + { + _stopwatch = Stopwatch.StartNew(); + _fixture = fixture; + _outputHelper = outputHelper; + _fixture.LoggedMessage += WriteMessage; + } + + private void WriteMessage(LogLevel logLevel, string category, EventId eventId, string message, Exception? exception) + { + var log = $"{_stopwatch.Elapsed.TotalSeconds:N3}s {category} - {logLevel}: {message}"; + if (exception != null) + { + log += Environment.NewLine + exception.ToString(); + } + _outputHelper.WriteLine(log); + } + + public void Dispose() + { + _fixture.LoggedMessage -= WriteMessage; + } + } +}