diff --git a/src/BuslyCLI.Console/BuslyCLI.Console.csproj b/src/BuslyCLI.Console/BuslyCLI.Console.csproj index 8d3ec80..242c178 100644 --- a/src/BuslyCLI.Console/BuslyCLI.Console.csproj +++ b/src/BuslyCLI.Console/BuslyCLI.Console.csproj @@ -34,6 +34,7 @@ + diff --git a/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs b/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs index 861a4ca..716d5e3 100644 --- a/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs +++ b/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs @@ -57,10 +57,14 @@ public string TransportConfigTypeToString(ITransportConfig transportConfig) return "learning"; case AzureServiceBusTransportConfig azureServiceBusConfig: return "azure-service-bus"; + case AzureStorageQueuesTransportConfig azureStorageQueuesTransportConfig: + return "azure-storage-queues"; case AmazonsqsTransportConfig amazonsqsTransportConfig: return "amazon-sqs"; case SqlServerTransportConfig sqlServerTransportConfig: return "sql-server"; + case PostgreSqlTransportConfig postgreSqlTransportConfig: + return "postgre-sql"; default: throw new ApplicationException("Unknown transport type"); } diff --git a/src/BuslyCLI.Console/Config/PostgreSqlTransportConfig.cs b/src/BuslyCLI.Console/Config/PostgreSqlTransportConfig.cs new file mode 100644 index 0000000..9632058 --- /dev/null +++ b/src/BuslyCLI.Console/Config/PostgreSqlTransportConfig.cs @@ -0,0 +1,6 @@ +namespace BuslyCLI.Config; + +public class PostgreSqlTransportConfig : ITransportConfig +{ + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/src/BuslyCLI.Console/Config/TransportConfig.cs b/src/BuslyCLI.Console/Config/TransportConfig.cs index 980ca7b..c75ab3a 100644 --- a/src/BuslyCLI.Console/Config/TransportConfig.cs +++ b/src/BuslyCLI.Console/Config/TransportConfig.cs @@ -11,6 +11,7 @@ public class TransportConfig public AzureServiceBusTransportConfig AzureServiceBusTransportConfig { get; set; } public AzureStorageQueuesTransportConfig AzureStorageQueuesTransportConfig { get; set; } public SqlServerTransportConfig SqlServerTransportConfig { get; set; } + public PostgreSqlTransportConfig PostgreSqlTransportConfig { get; set; } // Helper property to unify config access: [YamlIgnore] @@ -19,5 +20,6 @@ public class TransportConfig ?? (ITransportConfig)AmazonsqsTransportConfig ?? (ITransportConfig)AzureServiceBusTransportConfig ?? (ITransportConfig)AzureStorageQueuesTransportConfig - ?? SqlServerTransportConfig; + ?? (ITransportConfig)SqlServerTransportConfig + ?? PostgreSqlTransportConfig; } \ No newline at end of file diff --git a/src/BuslyCLI.Console/Config/Validators/PostgreSqlTransportConfigValidator.cs b/src/BuslyCLI.Console/Config/Validators/PostgreSqlTransportConfigValidator.cs new file mode 100644 index 0000000..bbb15c2 --- /dev/null +++ b/src/BuslyCLI.Console/Config/Validators/PostgreSqlTransportConfigValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace BuslyCLI.Config.Validators; + +public class PostgreSqlTransportConfigValidator : AbstractValidator +{ + public PostgreSqlTransportConfigValidator() + { + RuleFor(x => x.ConnectionString) + .NotEmpty(); + } +} \ No newline at end of file diff --git a/src/BuslyCLI.Console/Config/Validators/TransportConfigValidator.cs b/src/BuslyCLI.Console/Config/Validators/TransportConfigValidator.cs index 24a33c6..710f19d 100644 --- a/src/BuslyCLI.Console/Config/Validators/TransportConfigValidator.cs +++ b/src/BuslyCLI.Console/Config/Validators/TransportConfigValidator.cs @@ -16,7 +16,10 @@ public TransportConfigValidator() v.Add(new LearningTransportConfigValidator()); v.Add(new RabbitMQTransportConfigValidator()); v.Add(new AzureServiceBusTransportConfigValidator()); + v.Add(new AzureStorageQueuesTransportConfigValidator()); v.Add(new AmazonsqsTransportConfigValidator()); + v.Add(new SqlServerTransportConfigValidator()); + v.Add(new PostgreSqlTransportConfigValidator()); }); // RuleFor(x => x.LearningTransportConfig) diff --git a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs index 9e98f4c..0038dbf 100644 --- a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs @@ -36,7 +36,8 @@ private static IServiceCollection AddYamlDeserializer(this IServiceCollection se { "amazonsqs-transport-config", typeof(AmazonsqsTransportConfig) }, { "azure-service-bus-transport-config", typeof(AzureServiceBusTransportConfig) }, { "azure-storage-queues-transport-config", typeof(AzureStorageQueuesTransportConfig) }, - { "sql-server-transport-config", typeof(SqlServerTransportConfig) } + { "sql-server-transport-config", typeof(SqlServerTransportConfig) }, + { "postgre-sql-transport-config", typeof(PostgreSqlTransportConfig) } }; o.AddUniqueKeyTypeDiscriminator(keyMappings); diff --git a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs index c9125ba..9b6c6a4 100644 --- a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs +++ b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs @@ -36,6 +36,8 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) return CreateAmazonSQSTransport(amazonSqsTransportConfig); case SqlServerTransportConfig sqlServerTransportConfig: return CreateSqlServerTransport(sqlServerTransportConfig); + case PostgreSqlTransportConfig postgreSqlTransportConfig: + return CreatePostgreSqlTransport(postgreSqlTransportConfig); case LearningTransportConfig learningTransportConfig: return new LearningTransport { @@ -47,6 +49,12 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) } } + private TransportDefinition CreatePostgreSqlTransport(PostgreSqlTransportConfig postgreSqlTransportConfig) + { + var transport = new PostgreSqlTransport(postgreSqlTransportConfig.ConnectionString); + return transport; + } + private TransportDefinition CreateAzureStorageQueuesTransport(string connectionString) { var transport = new AzureStorageQueueTransport(connectionString); diff --git a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj index 37f9afa..b7026a4 100644 --- a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj +++ b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/tests/BuslyCLI.Console.Tests/Config/Validators/PostgreSqlTransportConfigValidatorTests.cs b/tests/BuslyCLI.Console.Tests/Config/Validators/PostgreSqlTransportConfigValidatorTests.cs new file mode 100644 index 0000000..0306295 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/Config/Validators/PostgreSqlTransportConfigValidatorTests.cs @@ -0,0 +1,47 @@ +using BuslyCLI.Config; +using BuslyCLI.Config.Validators; +using FluentValidation.TestHelper; + +namespace BuslyCLI.Console.Tests.Config.Validators; + +[TestFixture] +public class PostgreSqlTransportConfigValidatorTests +{ + private readonly PostgreSqlTransportConfigValidator _validator; + + public PostgreSqlTransportConfigValidatorTests() + { + _validator = new PostgreSqlTransportConfigValidator(); + } + + [Test] + public async Task ShouldErrorWhenConnectionStringIsNotPassed() + { + // Arrange + var postgreSqlTransportConfig = new PostgreSqlTransportConfig + { + ConnectionString = null + }; + // Act + var result = await _validator.TestValidateAsync(postgreSqlTransportConfig); + + // Assert + result.ShouldHaveValidationErrorFor(c => c.ConnectionString) + .WithErrorMessage("'Connection String' must not be empty."); + } + + [Test] + public async Task ShouldNotErrorConnectionStringIsPassed() + { + // Arrange + var postgreSqlTransportConfig = new PostgreSqlTransportConfig + { + ConnectionString = "Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true" + }; + // Act + var result = await _validator.TestValidateAsync(postgreSqlTransportConfig); + + // Assert + result.ShouldNotHaveValidationErrorFor(c => c.ConnectionString); + } +} \ No newline at end of file diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs index ca849f3..b53fd64 100644 --- a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs @@ -1,7 +1,6 @@ using Amazon.Runtime; using Amazon.SimpleNotificationService; using Amazon.SQS; -using NServiceBus.Settings; using NServiceBus.Transport; namespace BuslyCLI.Console.Tests.EndToEnd.Infrastructure; @@ -101,6 +100,13 @@ public async Task CreateSqlServerTestEndpoint(string sqlConnection return await InternalCreateTestEndpoint(name, transport); } + public async Task CreatePostgreSqlTransport(string connectionString) + { + var name = GenerateUniqueEndpointName(); + var transport = new PostgreSqlTransport(connectionString); + return await InternalCreateTestEndpoint(name, transport); + } + public async Task CreateAzureStorageQueuesTestEndpoint(string connectionString) { var name = GenerateUniqueEndpointName(); diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/PostgreSql/PostgreSqlEndToEndTestBase.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/PostgreSql/PostgreSqlEndToEndTestBase.cs new file mode 100644 index 0000000..c50e230 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/PostgreSql/PostgreSqlEndToEndTestBase.cs @@ -0,0 +1,20 @@ +using Testcontainers.PostgreSql; + +namespace BuslyCLI.Console.Tests.EndToEnd.PostgreSql; + +[TestFixture] +public abstract class PostgreSqlEndToEndTestBase : SingletonTestFixtureBase +{ + protected PostgreSqlContainer PostgreSqlContainer => Container; + + protected override PostgreSqlContainer CreateContainer() + { + return new PostgreSqlBuilder() + .Build(); + } + + protected override async Task StartContainerAsync(PostgreSqlContainer container) + { + await container.StartAsync(); + } +} \ No newline at end of file diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/PostgreSql/SendCommandPostgreSqlEndToEndTests.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/PostgreSql/SendCommandPostgreSqlEndToEndTests.cs new file mode 100644 index 0000000..fb65e95 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/PostgreSql/SendCommandPostgreSqlEndToEndTests.cs @@ -0,0 +1,120 @@ +using System.Text; +using System.Text.Json; +using BuslyCLI.Console.Tests.EndToEnd.Infrastructure; +using BuslyCLI.Console.Tests.TestHelpers; +using BuslyCLI.DependencyInjection; +using BuslyCLI.Spectre; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli.Extensions.DependencyInjection; +using Spectre.Console.Cli.Testing; + +namespace BuslyCLI.Console.Tests.EndToEnd.PostgreSql; + + +[TestFixture] +public class SendCommandPostgreSqlEndToEndTests : PostgreSqlEndToEndTestBase +{ + [SetUp] + public void Setup() + { + var registrations = new ServiceCollection(); + registrations.AddBuslyCLIServices(); + using var registrar = new DependencyInjectionRegistrar(registrations); + _sut = new CommandAppTester(registrar); + _sut.Configure(AppConfiguration.GetSpectreCommandConfiguration()); + } + + private CommandAppTester _sut; + + private readonly JsonSerializerOptions _jsonObjectOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; + + [Test] + public async Task ShouldSendCommand() + { + await RunWithTestEndpoint(async testEndpoint => + { + // Arrange + await testEndpoint.StartEndpoint(); + var messageBody = new { OrderNumber = Guid.NewGuid() }; + var json = JsonSerializer.Serialize(messageBody, _jsonObjectOptions); + var yamlFile = $""" + --- + current-transport: local-postgre-sql + transports: + - name: local-postgre-sql + postgre-sql-transport-config: + connection-string: {Container.GetConnectionString()} + """; + using var configFile = new TestableNServiceBusConfigurationFile(yamlFile); + + // Act + var result = _sut.Run( + "command", + "send", + "--content-type", "application/json", + "--enclosed-message-type", "MessageContracts.Commands.CreateOrder", + "--destination-endpoint", testEndpoint.EndpointName, + "--message-body", json, + "--config", configFile.FilePath); + + // Assert + Assert.That(result.ExitCode, Is.EqualTo(0)); + var message = testEndpoint.TryReceiveMessage(); + Assert.That(message.Headers["NServiceBus.EnclosedMessageTypes"], + Is.EqualTo("MessageContracts.Commands.CreateOrder")); + Assert.That(message.Headers["NServiceBus.ContentType"], Is.EqualTo("application/json")); + Assert.That(Encoding.UTF8.GetString(message.Body.Span), Is.EqualTo(json)); + }); + } + + [Test] + public async Task ShouldPublishEvent() + { + await RunWithTestEndpoint(async testEndpoint => + { + // Arrange + await testEndpoint.StartEndpoint(); + await testEndpoint.Subscribe("MessageContracts.Events.OrderCreated"); + var messageBody = new { OrderNumber = Guid.NewGuid() }; + var json = JsonSerializer.Serialize(messageBody, _jsonObjectOptions); + var yamlFile = $""" + --- + current-transport: local-postgre-sql + transports: + - name: local-postgre-sql + postgre-sql-transport-config: + connection-string: {Container.GetConnectionString()} + """; + using var configFile = new TestableNServiceBusConfigurationFile(yamlFile); + + // Act + var result = _sut.Run( + "event", + "publish", + "--content-type", "application/json", + "--enclosed-message-type", "MessageContracts.Events.OrderCreated", + "--message-body", json, + "--config", configFile.FilePath); + + // Assert + Assert.That(result.ExitCode, Is.EqualTo(0)); + var message = testEndpoint.TryReceiveMessage(); + Assert.That(message.Headers["NServiceBus.EnclosedMessageTypes"], + Is.EqualTo("MessageContracts.Events.OrderCreated")); + Assert.That(message.Headers["NServiceBus.ContentType"], Is.EqualTo("application/json")); + Assert.That(Encoding.UTF8.GetString(message.Body.Span), Is.EqualTo(json)); + }); + } + + // Test Endpoint + // Example of how to wait for and get messages + // https://github.com/Particular/NServiceBus.RabbitMQ/blob/dba627a5a2c50519d7a2466efe3f76c8d5c8828d/src/NServiceBus.Transport.RabbitMQ.Tests/RabbitMqContext.cs#L41 + private async Task RunWithTestEndpoint(Func testAction) + { + var testEndpoint = await new TestEndpointFactory().CreatePostgreSqlTransport(Container.GetConnectionString()); + + await testAction(testEndpoint); + await testEndpoint.ShutDownAndCleanUp(); + } +} \ No newline at end of file diff --git a/website/docs/transports/postgre-sql.md b/website/docs/transports/postgre-sql.md new file mode 100644 index 0000000..8f723cf --- /dev/null +++ b/website/docs/transports/postgre-sql.md @@ -0,0 +1,40 @@ +# Postgre Sql + +The **Postgre Sql Transport** is used to communicate to Postgre Sql. It is suitable for development, testing, and production environments. + +## Configuration + +To use the Postgre Sql Transport, define it under `transports` and reference it as `current-transport`. + +### Example + +```yaml +current-transport: local-postgre-sql + +transports: + - name: local-postgre-sql + postgre-sql-transport-config: + connection-string: Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true +``` + +--- + +## `postgre-sql-transport-config` Fields + +| Field | Required | Type | Default | Description | +| ------------------- | -------- | ------ | ------- | ----------------------------------- | +| `connection-string` | **Yes** | string | — | Full Postgre Sql Connection string. | + +--- + +## Field Details + +### `connection-string` (required) + +Postgre Sql connection string used to connect to the database. + +Examples: + +```yaml +connection-string: Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true +```