diff --git a/src/BuslyCLI.Console/BuslyCLI.Console.csproj b/src/BuslyCLI.Console/BuslyCLI.Console.csproj index 2c9d913..b9b9783 100644 --- a/src/BuslyCLI.Console/BuslyCLI.Console.csproj +++ b/src/BuslyCLI.Console/BuslyCLI.Console.csproj @@ -33,6 +33,7 @@ + diff --git a/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs b/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs index 358f590..861a4ca 100644 --- a/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs +++ b/src/BuslyCLI.Console/Commands/Transport/ListTransportsCommand.cs @@ -59,6 +59,8 @@ public string TransportConfigTypeToString(ITransportConfig transportConfig) return "azure-service-bus"; case AmazonsqsTransportConfig amazonsqsTransportConfig: return "amazon-sqs"; + case SqlServerTransportConfig sqlServerTransportConfig: + return "sql-server"; default: throw new ApplicationException("Unknown transport type"); } diff --git a/src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs b/src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs new file mode 100644 index 0000000..5992bfd --- /dev/null +++ b/src/BuslyCLI.Console/Config/SqlServerTransportConfig.cs @@ -0,0 +1,6 @@ +namespace BuslyCLI.Config; + +public class SqlServerTransportConfig : 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 7b5a938..7bc8da4 100644 --- a/src/BuslyCLI.Console/Config/TransportConfig.cs +++ b/src/BuslyCLI.Console/Config/TransportConfig.cs @@ -10,10 +10,13 @@ public class TransportConfig public AmazonsqsTransportConfig AmazonsqsTransportConfig { get; set; } public AzureServiceBusTransportConfig AzureServiceBusTransportConfig { get; set; } + public SqlServerTransportConfig SqlServerTransportConfig { get; set; } + // Helper property to unify config access: [YamlIgnore] public ITransportConfig Config => (ITransportConfig)LearningTransportConfig ?? (ITransportConfig)RabbitmqTransportConfig ?? (ITransportConfig)AmazonsqsTransportConfig - ?? (ITransportConfig)AzureServiceBusTransportConfig; + ?? (ITransportConfig)AzureServiceBusTransportConfig + ?? SqlServerTransportConfig; } \ No newline at end of file diff --git a/src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs b/src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs new file mode 100644 index 0000000..aed2082 --- /dev/null +++ b/src/BuslyCLI.Console/Config/Validators/SqlServerTransportConfigValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace BuslyCLI.Config.Validators; + +public class SqlServerTransportConfigValidator : AbstractValidator +{ + public SqlServerTransportConfigValidator() + { + RuleFor(x => x.ConnectionString) + .NotEmpty(); + } +} \ No newline at end of file diff --git a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs index e1ed09f..d1c350e 100644 --- a/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/BuslyCLI.Console/DependencyInjection/ServiceCollectionExtensions.cs @@ -33,7 +33,9 @@ private static IServiceCollection AddYamlDeserializer(this IServiceCollection se { { "learning-transport-config", typeof(LearningTransportConfig) }, { "rabbitmq-transport-config", typeof(RabbitmqTransportConfig) }, - { "amazonsqs-transport-config", typeof(AmazonsqsTransportConfig) } + { "amazonsqs-transport-config", typeof(AmazonsqsTransportConfig) }, + { "azure-service-bus-transport-config", typeof(AzureServiceBusTransportConfig) }, + { "sql-server-transport-config", typeof(SqlServerTransportConfig) } }; o.AddUniqueKeyTypeDiscriminator(keyMappings); diff --git a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs index 3ef3137..d0ad5db 100644 --- a/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs +++ b/src/BuslyCLI.Console/Factories/RawEndpointFactory.cs @@ -32,6 +32,8 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) return CreateAzureServiceBusTransport(azureServiceBusTransportConfig.ConnectionString); case AmazonsqsTransportConfig amazonSqsTransportConfig: return CreateAmazonSQSTransport(amazonSqsTransportConfig); + case SqlServerTransportConfig sqlServerTransportConfig: + return CreateSqlServerTransport(sqlServerTransportConfig); case LearningTransportConfig learningTransportConfig: return new LearningTransport { @@ -43,6 +45,11 @@ private TransportDefinition CreateTransport(TransportConfig transportConfig) } } + private TransportDefinition CreateSqlServerTransport(SqlServerTransportConfig sqlServerTransportConfig) + { + return new SqlServerTransport(sqlServerTransportConfig.ConnectionString); + } + private RabbitMQTransport CreateRabbitMQTransport(RabbitmqTransportConfig rabbitmqTransportConfig) { var t = new RabbitMQTransport(RoutingTopology.Conventional(QueueType.Quorum), rabbitmqTransportConfig.AmqpConnectionString); diff --git a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj index f9de6b7..89a262b 100644 --- a/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj +++ b/tests/BuslyCLI.Console.Tests/BuslyCLI.Console.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs b/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs index d5cf0cc..efa0d26 100644 --- a/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs +++ b/tests/BuslyCLI.Console.Tests/Config/Validators/AzureServiceBusTransportConfigValidatorTests.cs @@ -31,7 +31,7 @@ public async Task ShouldErrorWhenConnectionStringIsNotPassed() } [Test] - public async Task ShouldNotErrorStorageDirectoryIsPassed() + public async Task ShouldNotErrorConnectionStringIsPassed() { // Arrange var azureServiceBusTransportConfig = new AzureServiceBusTransportConfig diff --git a/tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs b/tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs new file mode 100644 index 0000000..5bf4553 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/Config/Validators/SqlServerTransportConfigValidatorTests.cs @@ -0,0 +1,47 @@ +using BuslyCLI.Config; +using BuslyCLI.Config.Validators; +using FluentValidation.TestHelper; + +namespace BuslyCLI.Console.Tests.Config.Validators; + +[TestFixture] +public class SqlServerTransportConfigValidatorTests +{ + private readonly SqlServerTransportConfigValidator _validator; + + public SqlServerTransportConfigValidatorTests() + { + _validator = new SqlServerTransportConfigValidator(); + } + + [Test] + public async Task ShouldErrorWhenConnectionStringIsNotPassed() + { + // Arrange + var sqlServerTransportConfig = new SqlServerTransportConfig + { + ConnectionString = null + }; + // Act + var result = await _validator.TestValidateAsync(sqlServerTransportConfig); + + // Assert + result.ShouldHaveValidationErrorFor(c => c.ConnectionString) + .WithErrorMessage("'Connection String' must not be empty."); + } + + [Test] + public async Task ShouldNotErrorConnectionStringIsPassed() + { + // Arrange + var sqlServerTransportConfig = new SqlServerTransportConfig + { + ConnectionString = "Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true" + }; + // Act + var result = await _validator.TestValidateAsync(sqlServerTransportConfig); + + // 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 35f7dbe..8052360 100644 --- a/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/Infrastructure/ITestEndpointFactory.cs @@ -90,4 +90,11 @@ private static async Task InternalCreateTestEndpoint(string endpoi return new TestEndpoint(infrastructure); } + + public async Task CreateSqlServerTestEndpoint(string sqlConnectionString) + { + var name = GenerateUniqueEndpointName(); + var transport = new SqlServerTransport(sqlConnectionString); + return await InternalCreateTestEndpoint(name, transport); + } } \ No newline at end of file diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs new file mode 100644 index 0000000..e3e4d85 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SendCommandSqlServerEndToEndTests.cs @@ -0,0 +1,119 @@ +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.SqlServer; + +[TestFixture] +public class SendCommandSqlServerEndToEndTests : SqlServerEndToEndTestBase +{ + [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-sql-server + transports: + - name: local-sql-server + sql-server-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-sql-server + transports: + - name: local-sql-server + sql-server-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().CreateSqlServerTestEndpoint(Container.GetConnectionString()); + + await testAction(testEndpoint); + await testEndpoint.ShutDownAndCleanUp(); + } +} \ No newline at end of file diff --git a/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs new file mode 100644 index 0000000..23f4b75 --- /dev/null +++ b/tests/BuslyCLI.Console.Tests/EndToEnd/SqlServer/SqlServerEndToEndTestBase.cs @@ -0,0 +1,21 @@ +using Microsoft.Data.SqlClient; +using Testcontainers.MsSql; + +namespace BuslyCLI.Console.Tests.EndToEnd.SqlServer; + +[TestFixture] +public abstract class SqlServerEndToEndTestBase : SingletonTestFixtureBase +{ + protected MsSqlContainer SqlServerContainer => Container; + + protected override MsSqlContainer CreateContainer() + { + return new MsSqlBuilder() + .Build(); + } + + protected override async Task StartContainerAsync(MsSqlContainer container) + { + await container.StartAsync(); + } +} \ No newline at end of file diff --git a/website/docs/transports/sql-server.md b/website/docs/transports/sql-server.md new file mode 100644 index 0000000..c6101e3 --- /dev/null +++ b/website/docs/transports/sql-server.md @@ -0,0 +1,40 @@ +# Sql Server + +The **Sql Server Transport** is used to communicate to Microsoft Sql Server. It is suitable for development, testing, and production environments. + +## Configuration + +To use the Sql Server Transport, define it under `transports` and reference it as `current-transport`. + +### Example + +```yaml +current-transport: local-sql-server + +transports: + - name: local-sql-server + sql-server-transport-config: + connection-string: Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true +``` + +--- + +## `sql-server-transport-config` Fields + +| Field | Required | Type | Default | Description | +| ------------------- | -------- | ------ | ------- | ---------------------------------- | +| `connection-string` | **Yes** | string | — | Full Sql Server Connection string. | + +--- + +## Field Details + +### `connection-string` (required) + +Sql Server connection string used to connect to Microsoft SQL Server. + +Examples: + +```yaml +connection-string: Data Source=(local);Initial Catalog=Ordering;Integrated Security=SSPI;Application Name=Busly-CLI;TrustServerCertificate=true +```