diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..c518eb7 --- /dev/null +++ b/Readme.md @@ -0,0 +1,59 @@ +# SMS Microservice + +The SMS Microservice is a simple microservice that acts as a wrapper around the API for a third-party SMS service. It listens for `SendSms` commands on a message queue, sends an HTTP request to the third-party SMS service, and then publishes an `SmsSent` event to a global event bus upon successful SMS delivery. + +## How to Build and Run the Application + +1. **Download the Files**: + +2. **Open the solution file**: + + - Navigate to the `SMSMicroservice` directory. + - Open the `SMSMicroservice.sln` solution file in Visual Studio. + +3. **Build the solution**: + + - Build the solution by clicking on the "Build" menu and selecting "Build Solution", or by pressing `Ctrl+Shift+B`. + +4. **Run the application**: + + - Open the `Program.cs` file in the `SMSMicroservice` project. + - Run the `Main` method. + +## How to Run Tests + +1. **Open the solution file**: + + - Navigate to the `SMSMicroservice` directory. + - Open the `SMSMicroservice.sln` solution file in Visual Studio. + +2. **Run the tests**: + + - Open the Test Explorer window by clicking on "Test" > "Test Explorer" from the top menu. + - Run all tests by clicking on "Run All" in the Test Explorer window. + +## Additional Information + +### Components Used + +- `IMessageQueue`: Interface for the message queue. +- `IEventBus`: Interface for the event bus. +- `ILogger`: Interface for the logger. +- `MessageQueue`: Concrete implementation of `IMessageQueue`. +- `EventBus`: Concrete implementation of `IEventBus`. +- `ConsoleLogger`: Concrete implementation of `ILogger`. +- `SendSmsCommand`: Command class for sending SMS. +- `SmsSentEvent`: Event class for successful SMS delivery. + +### Features + +- Asynchronous message-based communication. +- Reliable SMS delivery using an async flow. +- Basic error handling and logging. + +### Next Steps + +- Implement concrete implementations for the message queue, event bus, and logger. +- Write more extensive tests for the application logic. +- Implement retry logic for failed SMS deliveries. +- Improve error handling to handle edge cases and ensure graceful degradation. diff --git a/SMSMicroService.sln b/SMSMicroService.sln new file mode 100644 index 0000000..84d10fb --- /dev/null +++ b/SMSMicroService.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMSMicroService", "SMSMicroService\SMSMicroService.csproj", "{7C478F74-3D1C-4897-8F18-D6ABA883E53C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmsMicroservice.Tests", "SmsMicroservice.Tests\SmsMicroservice.Tests.csproj", "{258FD611-0DFB-48B9-84B4-19EC5DB6185D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7C478F74-3D1C-4897-8F18-D6ABA883E53C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C478F74-3D1C-4897-8F18-D6ABA883E53C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C478F74-3D1C-4897-8F18-D6ABA883E53C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C478F74-3D1C-4897-8F18-D6ABA883E53C}.Release|Any CPU.Build.0 = Release|Any CPU + {258FD611-0DFB-48B9-84B4-19EC5DB6185D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {258FD611-0DFB-48B9-84B4-19EC5DB6185D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {258FD611-0DFB-48B9-84B4-19EC5DB6185D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {258FD611-0DFB-48B9-84B4-19EC5DB6185D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AD5ECFDB-20BA-4998-A637-FE9B8711FED6} + EndGlobalSection +EndGlobal diff --git a/SMSMicroService/Helper/PhoneNumberHelper.cs b/SMSMicroService/Helper/PhoneNumberHelper.cs new file mode 100644 index 0000000..9dd44cb --- /dev/null +++ b/SMSMicroService/Helper/PhoneNumberHelper.cs @@ -0,0 +1,11 @@ +namespace SMSMicroService.Helper +{ + internal class PhoneNumberHelper + { + public static string GenerateRandomPhoneNumber() + { + Random random = new(); + return string.Format("{0:000}{1:000}{2:0000}", random.Next(700, 999), random.Next(0, 999), random.Next(0, 9999)); + } + } +} diff --git a/SMSMicroService/Helper/TextMessageHelper.cs b/SMSMicroService/Helper/TextMessageHelper.cs new file mode 100644 index 0000000..20283da --- /dev/null +++ b/SMSMicroService/Helper/TextMessageHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SMSMicroService.Helper +{ + internal class TextMessageHelper + { + public static string GenerateRandomTextMessage() + { + Random random = new(); + string[] messages = + [ + "Thank you for choosing us! We appreciate your business.", + "Hi! Just a quick note to say thank you for your support.", + "Your satisfaction is our top priority. Let us know if you need assistance.", + "Hello! We're here to provide you with excellent service.", + "Thanks for being a valued customer! Have a great day!", + "We're grateful for your trust in us. Have a fantastic day!", + "Dear client, your feedback is important to us. Let us know how we're doing!", + "Thanks for choosing us. We're committed to your satisfaction.", + "Hello! We appreciate your business. Wishing you a wonderful day!", + "Your satisfaction is important to us. We're here to help!" + ]; + + return messages[random.Next(0,9)]; + } + } +} diff --git a/SMSMicroService/Implementations/ConsoleLogger.cs b/SMSMicroService/Implementations/ConsoleLogger.cs new file mode 100644 index 0000000..2aea94a --- /dev/null +++ b/SMSMicroService/Implementations/ConsoleLogger.cs @@ -0,0 +1,17 @@ +using SMSMicroService.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SMSMicroService.Implementations +{ + public class ConsoleLogger: ILogger + { + public void Log(string message) + { + Console.WriteLine($"[{DateTime.UtcNow}] {message}"); + } + } +} diff --git a/SMSMicroService/Implementations/EventBus.cs b/SMSMicroService/Implementations/EventBus.cs new file mode 100644 index 0000000..4721910 --- /dev/null +++ b/SMSMicroService/Implementations/EventBus.cs @@ -0,0 +1,28 @@ +using SMSMicroService.Interfaces; + + +namespace SMSMicroService.Implementations +{ + public class EventBus: IEventBus + { + private readonly List _publishedEvents; + + public EventBus() + { + _publishedEvents = []; + } + + public async Task PublishEventAsync(T evt) + { + if (evt != null) { + _publishedEvents.Add(evt); + } + await Task.Delay(0); + } + + public List GetPublishedEvents() + { + return _publishedEvents; + } + } +} diff --git a/SMSMicroService/Implementations/MessageQueue.cs b/SMSMicroService/Implementations/MessageQueue.cs new file mode 100644 index 0000000..32e8c86 --- /dev/null +++ b/SMSMicroService/Implementations/MessageQueue.cs @@ -0,0 +1,48 @@ +using SMSMicroService.Interfaces; +using System.Collections.Concurrent; +using SMSMicroService.Helper; + +namespace SMSMicroService.Implementations +{ + public class MessageQueue: IMessageQueue + { + private readonly ConcurrentQueue _queue; + + public MessageQueue() + { + _queue = new ConcurrentQueue(); + } + + public void SendMessage(T message) + { + if (message != null) + { + _queue.Enqueue(message); + + } + } + + public async Task ReceiveMessageAsync() + { + while (true) + { + if (_queue.TryDequeue(out var message)) + { + return (T)message; + } + await Task.Delay(1000); + } + } + + public void StartAddingRandomMessagesAsync() + { + while (true) + { + var phoneNumber = PhoneNumberHelper.GenerateRandomPhoneNumber(); + var smsText = TextMessageHelper.GenerateRandomTextMessage(); + SendMessage(new SendSmsCommand { PhoneNumber = phoneNumber, SmsText = smsText }); + Thread.Sleep(1500); + } + } + } +} diff --git a/SMSMicroService/Interfaces/IEventBus.cs b/SMSMicroService/Interfaces/IEventBus.cs new file mode 100644 index 0000000..19b933d --- /dev/null +++ b/SMSMicroService/Interfaces/IEventBus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SMSMicroService.Interfaces +{ + public interface IEventBus + { + Task PublishEventAsync(T @event); + } +} diff --git a/SMSMicroService/Interfaces/ILogger.cs b/SMSMicroService/Interfaces/ILogger.cs new file mode 100644 index 0000000..05003e9 --- /dev/null +++ b/SMSMicroService/Interfaces/ILogger.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SMSMicroService.Interfaces +{ + public interface ILogger + { + void Log(string message); + } +} diff --git a/SMSMicroService/Interfaces/IMessageQueue.cs b/SMSMicroService/Interfaces/IMessageQueue.cs new file mode 100644 index 0000000..351fbe4 --- /dev/null +++ b/SMSMicroService/Interfaces/IMessageQueue.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SMSMicroService.Interfaces +{ + public interface IMessageQueue + { + Task ReceiveMessageAsync(); + public void SendMessage(T message); + } +} diff --git a/SMSMicroService/Program.cs b/SMSMicroService/Program.cs new file mode 100644 index 0000000..834ca65 --- /dev/null +++ b/SMSMicroService/Program.cs @@ -0,0 +1,18 @@ +using SMSMicroService.Implementations; + +namespace SMSMicroService +{ + internal class Program + { + static async Task Main(string[] args) + { + var messageQueue = new MessageQueue(); + var eventBus = new EventBus(); + var logger = new ConsoleLogger(); + var smsMicroservice = new SmsMicroservice(messageQueue, eventBus, logger); + Thread addRandomMessages = new(() => messageQueue.StartAddingRandomMessagesAsync()); + addRandomMessages.Start(); + await smsMicroservice.Start(); + } + } +} diff --git a/SMSMicroService/SMSMicroService.csproj b/SMSMicroService/SMSMicroService.csproj new file mode 100644 index 0000000..e976d42 --- /dev/null +++ b/SMSMicroService/SMSMicroService.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/SMSMicroService/SendSmsCommand.cs b/SMSMicroService/SendSmsCommand.cs new file mode 100644 index 0000000..ed3cd11 --- /dev/null +++ b/SMSMicroService/SendSmsCommand.cs @@ -0,0 +1,9 @@ +namespace SMSMicroService +{ + public class SendSmsCommand + { + public required string PhoneNumber { get; set; } + public required string SmsText { get; set; } + + } +} diff --git a/SMSMicroService/SmsMicroservice.cs b/SMSMicroService/SmsMicroservice.cs new file mode 100644 index 0000000..8ed3d0f --- /dev/null +++ b/SMSMicroService/SmsMicroservice.cs @@ -0,0 +1,60 @@ +using SMSMicroService.Interfaces; + +namespace SMSMicroService +{ + public class SmsMicroservice + { + private readonly IMessageQueue _messageQueue; + private readonly IEventBus _eventBus; + private readonly ILogger _logger; + public SmsMicroservice(IMessageQueue messageQueue, IEventBus eventBus, ILogger logger) + { + _messageQueue = messageQueue; + _eventBus = eventBus; + _logger = logger; + } + + public async Task Start() + { + _logger.Log("SMS Microservice started."); + + while (true) + { + var command = await _messageQueue.ReceiveMessageAsync(); + + _logger.Log($"Received SendSms command for phone number: {command.PhoneNumber}"); + + var smsSentEvent = await SendSmsAsync(command); + + await _eventBus.PublishEventAsync(smsSentEvent); + } + } + + private async Task SendSmsAsync(SendSmsCommand command) + { + try + { + using var httpClient = new HttpClient(); + + var requestContent = new StringContent($"{{'PhoneNumber': '{command.PhoneNumber}', 'SmsText': '{command.SmsText}'}}"); + var response = await httpClient.PostAsync("https://4kvv1.wiremockapi.cloud/sendSms", requestContent); + + if (!response.IsSuccessStatusCode) + { + _logger.Log($"Failed to send SMS to {command.PhoneNumber}. Status code: {response.StatusCode}"); + return new SmsSentEvent { SmsSent = false, PhoneNumber = command.PhoneNumber, SmsText = command.SmsText, Timestamp = DateTime.UtcNow }; + // Retry can be implemented + } + + _logger.Log($"SMS sent successfully to {command.PhoneNumber}"); + + return new SmsSentEvent { SmsSent = true, PhoneNumber = command.PhoneNumber, SmsText = command.SmsText, Timestamp = DateTime.UtcNow }; + } + catch (Exception ex) + { + _logger.Log($"Error sending SMS to {command.PhoneNumber}: {ex.Message}"); + throw; + } + } + } +} diff --git a/SMSMicroService/SmsSentEvent.cs b/SMSMicroService/SmsSentEvent.cs new file mode 100644 index 0000000..2782e67 --- /dev/null +++ b/SMSMicroService/SmsSentEvent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SMSMicroService +{ + public class SmsSentEvent + { + public required bool SmsSent { get; set; } + public required string PhoneNumber { get; set; } + public required string SmsText { get; set; } + public required DateTime Timestamp { get; set; } + } +} diff --git a/SmsMicroservice.Tests/EventBusTests.cs b/SmsMicroservice.Tests/EventBusTests.cs new file mode 100644 index 0000000..fc3167b --- /dev/null +++ b/SmsMicroservice.Tests/EventBusTests.cs @@ -0,0 +1,52 @@ +using SMSMicroService; +using SMSMicroService.Implementations; + +namespace SmsMicroservice.Tests +{ + public class EventBusTests + { + private readonly EventBus _eventBus; + public EventBusTests() { + _eventBus = new EventBus(); + } + + [Fact] + public async Task PublishEventAsync_AddsSuccessEventToList() + { + // Arrange + var testEvent = new SmsSentEvent() + { + SmsSent = true, + PhoneNumber = "7006789876", + SmsText = "Hello from test", + Timestamp = DateTime.Now + }; + + // Act + await _eventBus.PublishEventAsync(testEvent); + + // Assert + Assert.Contains(testEvent, _eventBus.GetPublishedEvents()); + } + + [Fact] + public async Task PublishEventAsync_AddsFailureEventToList() + { + // Arrange + var testEvent = new SmsSentEvent() + { + SmsSent = false, + PhoneNumber = "7006789876", + SmsText = "Hello from test", + Timestamp = DateTime.Now + }; + + // Act + await _eventBus.PublishEventAsync(testEvent); + + // Assert + Assert.Contains(testEvent, _eventBus.GetPublishedEvents()); + } + + } +} diff --git a/SmsMicroservice.Tests/SmsMicroservice.Tests.csproj b/SmsMicroservice.Tests/SmsMicroservice.Tests.csproj new file mode 100644 index 0000000..7269cb5 --- /dev/null +++ b/SmsMicroservice.Tests/SmsMicroservice.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/SmsMicroservice.Tests/SmsServiceTests.cs b/SmsMicroservice.Tests/SmsServiceTests.cs new file mode 100644 index 0000000..0ea0d60 --- /dev/null +++ b/SmsMicroservice.Tests/SmsServiceTests.cs @@ -0,0 +1,65 @@ +using SMSMicroService; +using SMSMicroService.Implementations; + +namespace SmsMicroservice.Tests +{ + public class SmsMicroserviceTests + { + private readonly SMSMicroService.SmsMicroservice _smsMicroservice; + private readonly EventBus _eventBus; + + + public SmsMicroserviceTests() + { + _eventBus = new EventBus(); + _smsMicroservice = new SMSMicroService.SmsMicroservice(new MessageQueue(), _eventBus, new ConsoleLogger()); + } + + private async Task InvokeSendSmsAsync(SendSmsCommand command) + { + var methodInfo = typeof(SMSMicroService.SmsMicroservice).GetMethod("SendSmsAsync", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var task = methodInfo != null ? methodInfo.Invoke(_smsMicroservice, new object[] { command }) as Task : throw new MethodAccessException("Method not found"); + return task == null ? throw new InvalidOperationException("Command should not be NULL") : await task; + } + + [Fact] + public async Task SendSmsAsync_ValidCommand_ReturnsSmsSentEvent() + { + // Arrange + var command = new SendSmsCommand + { + PhoneNumber = "7001239875", + SmsText = "Hello from Test Project" + }; + + // Act + var result = await InvokeSendSmsAsync(command); + + // Assert + Assert.NotNull(result); + Assert.True(result.SmsSent); + Assert.Equal(command.PhoneNumber, result.PhoneNumber); + Assert.Equal(command.SmsText, result.SmsText); + } + + [Fact] + public async Task SendSmsAsync_InValidCommand_ReturnsSmsSentEvent() + { + // Arrange + var command = new SendSmsCommand + { + PhoneNumber = "1234567890", // Invalid PhoneNumber + SmsText = "Hello from ABC!" // Invalid TextMessage + }; + + // Act + var result = await InvokeSendSmsAsync(command); + + // Assert + Assert.NotNull(result); + Assert.False(result.SmsSent); + Assert.Equal(command.PhoneNumber, result.PhoneNumber); + Assert.Equal(command.SmsText, result.SmsText); + } + } +}