diff --git a/src/Altinn.Notifications.Sms.Core/Altinn.Notifications.Sms.Core.csproj b/src/Altinn.Notifications.Sms.Core/Altinn.Notifications.Sms.Core.csproj
index c08abc7..d3a04b3 100644
--- a/src/Altinn.Notifications.Sms.Core/Altinn.Notifications.Sms.Core.csproj
+++ b/src/Altinn.Notifications.Sms.Core/Altinn.Notifications.Sms.Core.csproj
@@ -3,6 +3,7 @@
net8.0
enable
+ true
enable
{184897A2-F52D-400A-BD50-FEB6554845AF}
diff --git a/src/Altinn.Notifications.Sms.Core/Configuration/ServiceCollectionExtensions.cs b/src/Altinn.Notifications.Sms.Core/Configuration/ServiceCollectionExtensions.cs
index 97d9668..fa0ec8c 100644
--- a/src/Altinn.Notifications.Sms.Core/Configuration/ServiceCollectionExtensions.cs
+++ b/src/Altinn.Notifications.Sms.Core/Configuration/ServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
-using Microsoft.Extensions.Configuration;
+using Altinn.Notifications.Sms.Core.Sending;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Altinn.Notifications.Sms.Core.Configuration;
@@ -16,6 +17,15 @@ public static class ServiceCollectionExtensions
/// The given service collection.
public static IServiceCollection AddCoreServices(this IServiceCollection services, IConfiguration config)
{
+ TopicSettings topicSettings = config!.GetSection("KafkaSettings").Get()!;
+
+ if (topicSettings == null)
+ {
+ throw new ArgumentNullException(nameof(config), "Required Kafka settings is missing from application configuration");
+ }
+
+ services.AddSingleton();
+ services.AddSingleton(topicSettings);
return services;
}
}
diff --git a/src/Altinn.Notifications.Sms.Core/Configuration/TopicSettings.cs b/src/Altinn.Notifications.Sms.Core/Configuration/TopicSettings.cs
new file mode 100644
index 0000000..0452f3a
--- /dev/null
+++ b/src/Altinn.Notifications.Sms.Core/Configuration/TopicSettings.cs
@@ -0,0 +1,12 @@
+namespace Altinn.Notifications.Sms.Core.Configuration;
+
+///
+/// Configuration object used to hold topic names for core services to publish to in Kafka.
+///
+public class TopicSettings
+{
+ ///
+ /// The name of the sms status updated topic
+ ///
+ public string SmsStatusUpdatedTopicName { get; set; } = string.Empty;
+}
diff --git a/src/Altinn.Notifications.Sms.Core/Sending/ISendingService.cs b/src/Altinn.Notifications.Sms.Core/Sending/ISendingService.cs
new file mode 100644
index 0000000..a90faba
--- /dev/null
+++ b/src/Altinn.Notifications.Sms.Core/Sending/ISendingService.cs
@@ -0,0 +1,14 @@
+namespace Altinn.Notifications.Sms.Core.Sending;
+
+///
+/// Describes the required public method of the sms service.
+///
+public interface ISendingService
+{
+ ///
+ /// Send an sms
+ ///
+ /// The details for an sms to be sent.
+ /// A task representing the asynchronous operation
+ Task SendAsync(Sms sms);
+}
diff --git a/src/Altinn.Notifications.Sms.Core/Sending/SendingService.cs b/src/Altinn.Notifications.Sms.Core/Sending/SendingService.cs
new file mode 100644
index 0000000..6c465bb
--- /dev/null
+++ b/src/Altinn.Notifications.Sms.Core/Sending/SendingService.cs
@@ -0,0 +1,56 @@
+using Altinn.Notifications.Sms.Core.Configuration;
+using Altinn.Notifications.Sms.Core.Dependencies;
+using Altinn.Notifications.Sms.Core.Shared;
+using Altinn.Notifications.Sms.Core.Status;
+
+namespace Altinn.Notifications.Sms.Core.Sending;
+
+///
+/// Service responsible for handling sms sending requests.
+///
+public class SendingService : ISendingService
+{
+ private readonly ISmsClient _smsClient;
+ private readonly TopicSettings _settings;
+ private readonly ICommonProducer _producer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A client that can perform actual sms sending.
+ /// A kafka producer.
+ /// The topic settings.
+ public SendingService(ISmsClient smsClient, ICommonProducer producer, TopicSettings settings)
+ {
+ _smsClient = smsClient;
+ _producer = producer;
+ _settings = settings;
+ }
+
+ ///
+ public async Task SendAsync(Sms sms)
+ {
+ Result result = await _smsClient.SendAsync(sms);
+
+ SendOperationResult operationResult = new SendOperationResult
+ {
+ NotificationId = sms.NotificationId,
+ };
+
+ await result.Match(
+ async gatewayReference =>
+ {
+ operationResult.GatewayReference = gatewayReference;
+ operationResult.SendResult = SmsSendResult.Accepted;
+
+ await _producer.ProduceAsync(_settings.SmsStatusUpdatedTopicName, operationResult.Serialize());
+ },
+ async smsSendFailResponse =>
+ {
+ operationResult.GatewayReference = string.Empty;
+ operationResult.SendResult = smsSendFailResponse.SendResult;
+
+ await _producer.ProduceAsync(_settings.SmsStatusUpdatedTopicName, operationResult.Serialize());
+ });
+ }
+}
diff --git a/src/Altinn.Notifications.Sms.Core/Sending/Sms.cs b/src/Altinn.Notifications.Sms.Core/Sending/Sms.cs
index 114dd76..dc7051e 100644
--- a/src/Altinn.Notifications.Sms.Core/Sending/Sms.cs
+++ b/src/Altinn.Notifications.Sms.Core/Sending/Sms.cs
@@ -1,26 +1,83 @@
-namespace Altinn.Notifications.Sms.Core.Sending
+using System.Text.Json;
+
+namespace Altinn.Notifications.Sms.Core.Sending;
+
+///
+/// Class representing an sms message
+///
+public class Sms
{
///
- /// Class representing an sms message
+ /// Gets or sets the id of the sms.
+ ///
+ public Guid NotificationId { get; set; }
+
+ ///
+ /// Gets or sets the recipient of the sms message
+ ///
+ public string Recipient { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the sender of the sms message
+ ///
+ ///
+ /// Can be a literal string or a phone number
+ ///
+ public string Sender { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the contents of the sms message
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// Initializes a new instance of the class.
///
- public class Sms
+ public Sms(Guid notificationId, string recipient, string sender, string message)
{
- ///
- /// Gets or sets the contents of the sms message
- ///
- public string Message { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the recipient of the sms message
- ///
- public string Recipient { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the sender of the sms message
- ///
- ///
- /// Can be a literal string or a phone number
- ///
- public string Sender { get; set; } = string.Empty;
+ NotificationId = notificationId;
+ Recipient = recipient;
+ Sender = sender;
+ Message = message;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Sms()
+ {
+ }
+
+ ///
+ /// Try to parse a json string into a
+ ///
+ public static bool TryParse(string input, out Sms value)
+ {
+ Sms? parsedOutput;
+ value = new Sms();
+
+ if (string.IsNullOrEmpty(input))
+ {
+ return false;
+ }
+
+ try
+ {
+ parsedOutput = JsonSerializer.Deserialize(
+ input!,
+ new JsonSerializerOptions()
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ value = parsedOutput!;
+ return value.NotificationId != Guid.Empty;
+ }
+ catch
+ {
+ // try parse, we simply return false if fails
+ }
+
+ return false;
}
}
diff --git a/src/Altinn.Notifications.Sms.Core/Status/SendOperationResult.cs b/src/Altinn.Notifications.Sms.Core/Status/SendOperationResult.cs
new file mode 100644
index 0000000..bd83596
--- /dev/null
+++ b/src/Altinn.Notifications.Sms.Core/Status/SendOperationResult.cs
@@ -0,0 +1,40 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Altinn.Notifications.Sms.Core.Status;
+
+///
+/// A class representing a send operation update object
+///
+public class SendOperationResult
+{
+ ///
+ /// The notification id
+ ///
+ public Guid NotificationId { get; set; }
+
+ ///
+ /// The reference to the sending in sms gateway
+ ///
+ public string GatewayReference { get; set; } = string.Empty;
+
+ ///
+ /// The sms send result
+ ///
+ public SmsSendResult? SendResult { get; set; }
+
+ ///
+ /// Json serializes the
+ ///
+ public string Serialize()
+ {
+ return JsonSerializer.Serialize(
+ this,
+ new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
+ });
+ }
+}
diff --git a/src/Altinn.Notifications.Sms.Core/Status/SmsSendResult.cs b/src/Altinn.Notifications.Sms.Core/Status/SmsSendResult.cs
index ff4edbc..7e174b9 100644
--- a/src/Altinn.Notifications.Sms.Core/Status/SmsSendResult.cs
+++ b/src/Altinn.Notifications.Sms.Core/Status/SmsSendResult.cs
@@ -5,9 +5,24 @@
///
public enum SmsSendResult
{
+ ///
+ /// Sms send operation running
+ ///
Sending,
+
+ ///
+ /// Sms send operation accepted
+ ///
Accepted,
+
+ ///
+ /// Sms send operation failed
+ ///
Failed,
+
+ ///
+ /// Sms send operation failed due to invalid receiver
+ ///
Failed_InvalidReceiver
}
}
diff --git a/src/Altinn.Notifications.Sms.Integrations/Altinn.Notifications.Sms.Integrations.csproj b/src/Altinn.Notifications.Sms.Integrations/Altinn.Notifications.Sms.Integrations.csproj
index 5185758..9e94b77 100644
--- a/src/Altinn.Notifications.Sms.Integrations/Altinn.Notifications.Sms.Integrations.csproj
+++ b/src/Altinn.Notifications.Sms.Integrations/Altinn.Notifications.Sms.Integrations.csproj
@@ -3,6 +3,7 @@
net8.0
enable
+ true
enable
{4ED9641C-6C11-4F23-8CFB-5E0B445643C2}
diff --git a/src/Altinn.Notifications.Sms.Integrations/Consumers/SendSmsQueueConsumer.cs b/src/Altinn.Notifications.Sms.Integrations/Consumers/SendSmsQueueConsumer.cs
index 2c489c2..6df1440 100644
--- a/src/Altinn.Notifications.Sms.Integrations/Consumers/SendSmsQueueConsumer.cs
+++ b/src/Altinn.Notifications.Sms.Integrations/Consumers/SendSmsQueueConsumer.cs
@@ -1,5 +1,6 @@
using Altinn.Notifications.Integrations.Kafka.Consumers;
using Altinn.Notifications.Sms.Core.Dependencies;
+using Altinn.Notifications.Sms.Core.Sending;
using Altinn.Notifications.Sms.Integrations.Configuration;
using Microsoft.Extensions.Logging;
@@ -10,6 +11,7 @@ namespace Altinn.Notifications.Sms.Integrations.Consumers;
///
public sealed class SendSmsQueueConsumer : KafkaConsumerBase
{
+ private readonly ISendingService _sendingService;
private readonly ICommonProducer _producer;
private readonly ILogger _logger;
private readonly string _retryTopicName;
@@ -19,10 +21,12 @@ public sealed class SendSmsQueueConsumer : KafkaConsumerBase
///
public SendSmsQueueConsumer(
KafkaSettings kafkaSettings,
+ ISendingService sendingService,
ICommonProducer producer,
ILogger logger)
: base(kafkaSettings, logger, kafkaSettings.SendSmsQueueTopicName)
{
+ _sendingService = sendingService;
_producer = producer;
_retryTopicName = kafkaSettings.SendSmsQueueRetryTopicName;
_logger = logger;
@@ -36,8 +40,16 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)
private async Task ConsumeSms(string message)
{
- _logger.LogInformation($"// SendSmsQueueConsumer // ConsumeSms // Consuming message: {message}");
- await Task.CompletedTask;
+ bool succeeded = Core.Sending.Sms.TryParse(message, out Core.Sending.Sms sms);
+
+ if (!succeeded)
+ {
+ _logger.LogError("// SendSmsQueueConsumer // ConsumeSms // Deserialization of message failed. {Message}", message);
+
+ return;
+ }
+
+ await _sendingService.SendAsync(sms);
}
private async Task RetrySms(string message)
diff --git a/src/Altinn.Notifications.Sms.Integrations/LinkMobility/SmsClient.cs b/src/Altinn.Notifications.Sms.Integrations/LinkMobility/SmsClient.cs
index 97b5697..49335d4 100644
--- a/src/Altinn.Notifications.Sms.Integrations/LinkMobility/SmsClient.cs
+++ b/src/Altinn.Notifications.Sms.Integrations/LinkMobility/SmsClient.cs
@@ -19,7 +19,7 @@ public class SmsClient : ISmsClient
///
/// Initializes a new instance of the class.
///
- /// The configuration for the sms gateway
+ /// Gateway Client
public SmsClient(IAltinnGatewayClient client)
{
_client = client;
diff --git a/src/Altinn.Notifications.Sms/Program.cs b/src/Altinn.Notifications.Sms/Program.cs
index 6e657a0..2c27c6c 100644
--- a/src/Altinn.Notifications.Sms/Program.cs
+++ b/src/Altinn.Notifications.Sms/Program.cs
@@ -111,10 +111,4 @@ void Configure()
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
-
- if (app.Environment.IsDevelopment())
- {
- app.UseSwagger();
- app.UseSwaggerUI();
- }
}
diff --git a/src/Altinn.Notifications.Sms/appsettings.json b/src/Altinn.Notifications.Sms/appsettings.json
index 178b0b6..4cc0e44 100644
--- a/src/Altinn.Notifications.Sms/appsettings.json
+++ b/src/Altinn.Notifications.Sms/appsettings.json
@@ -20,11 +20,10 @@
"SaslUsername": null,
"SaslPassword": null
},
- "HealthCheckTopicName": "altinn.notifications.email.health.check",
- "EmailSendingAcceptedTopicName": "altinn.notifications.email.sending.accepted",
- "EmailStatusUpdatedTopicName": "altinn.notifications.email.status.updated",
+ "HealthCheckTopicName": "altinn.notifications.sms.health.check",
+ "SmsStatusUpdatedTopicName": "altinn.notifications.sms.status.updated",
"SendSmsQueueTopicName": "altinn.notifications.sms.queue",
- "SendEmailQueueRetryTopicName": "altinn.notifications.email.queue.retry",
+ "SendSmsQueueRetryTopicName": "altinn.notifications.sms.queue.retry",
"AltinnServiceUpdateTopicName ": "altinn.platform.service.updated"
},
"SmsGatewayConfiguration": {
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/Altinn.Notifications.Sms.IntegrationTests.csproj b/test/Altinn.Notifications.Sms.IntegrationTests/Altinn.Notifications.Sms.IntegrationTests.csproj
index 2342a11..48b9936 100644
--- a/test/Altinn.Notifications.Sms.IntegrationTests/Altinn.Notifications.Sms.IntegrationTests.csproj
+++ b/test/Altinn.Notifications.Sms.IntegrationTests/Altinn.Notifications.Sms.IntegrationTests.csproj
@@ -9,7 +9,9 @@
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -22,8 +24,6 @@
-
-
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/Consumers/SendSmsQueueConsumerTests.cs b/test/Altinn.Notifications.Sms.IntegrationTests/Consumers/SendSmsQueueConsumerTests.cs
new file mode 100644
index 0000000..79d11b3
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.IntegrationTests/Consumers/SendSmsQueueConsumerTests.cs
@@ -0,0 +1,115 @@
+using System.Text.Json;
+
+using Altinn.Notifications.Sms.Core.Dependencies;
+using Altinn.Notifications.Sms.Core.Sending;
+using Altinn.Notifications.Sms.Integrations.Configuration;
+using Altinn.Notifications.Sms.Integrations.Consumers;
+using Altinn.Notifications.Sms.Integrations.Producers;
+using Altinn.Notifications.Sms.IntegrationTests.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+using Moq;
+
+namespace Altinn.Notifications.Sms.IntegrationTests.Consumers;
+
+public class SendSmsQueueConsumerTests : IAsyncLifetime
+{
+ private readonly string _sendSmsQueueTopicName = Guid.NewGuid().ToString();
+ private readonly string _sendSmsQueueRetryTopicName = Guid.NewGuid().ToString();
+ private readonly KafkaSettings _kafkaSettings;
+ private ServiceProvider? _serviceProvider;
+
+ public SendSmsQueueConsumerTests()
+ {
+ _kafkaSettings = new KafkaSettings
+ {
+ BrokerAddress = "localhost:9092",
+ Consumer = new()
+ {
+ GroupId = "sms-sending-consumer"
+ },
+ SendSmsQueueTopicName = _sendSmsQueueTopicName,
+ Admin = new()
+ {
+ TopicList = new List { _sendSmsQueueTopicName, _sendSmsQueueRetryTopicName }
+ }
+ };
+ }
+
+ public async Task InitializeAsync()
+ {
+ await Task.CompletedTask;
+ }
+
+ public async Task DisposeAsync()
+ {
+ await KafkaUtil.DeleteTopicAsync(_sendSmsQueueTopicName);
+ await KafkaUtil.DeleteTopicAsync(_sendSmsQueueRetryTopicName);
+ }
+
+ [Fact]
+ public async Task ConsumeSmsTest_Successfull_deserialization_of_message_Service_called_once()
+ {
+ // Arrange
+ var sendingServiceMock = new Mock();
+ sendingServiceMock.Setup(s => s.SendAsync(It.IsAny())).Returns(Task.CompletedTask);
+
+ Core.Sending.Sms sms = new(Guid.NewGuid(), "recipient", "sender", "message");
+
+ using SendSmsQueueConsumer queueConsumer = GetSmsSendingConsumer(sendingServiceMock.Object);
+ using CommonProducer commonProducer = KafkaUtil.GetKafkaProducer(_serviceProvider!);
+
+ // Act
+ await commonProducer.ProduceAsync(_sendSmsQueueTopicName, JsonSerializer.Serialize(sms));
+
+ await queueConsumer.StartAsync(CancellationToken.None);
+ await Task.Delay(10000);
+ await queueConsumer.StopAsync(CancellationToken.None);
+
+ // Assert
+ sendingServiceMock.Verify(s => s.SendAsync(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task ConsumeSmsTest_Deserialization_of_message_fails_Never_calls_service()
+ {
+ // Arrange
+ var sendingServiceMock = new Mock();
+ sendingServiceMock.Setup(s => s.SendAsync(It.IsAny())).Returns(Task.CompletedTask);
+
+ using SendSmsQueueConsumer queueConsumer = GetSmsSendingConsumer(sendingServiceMock.Object);
+ using CommonProducer commonProducer = KafkaUtil.GetKafkaProducer(_serviceProvider!);
+
+ // Act
+ await commonProducer.ProduceAsync(_sendSmsQueueTopicName, JsonSerializer.Serialize("Crap sms"));
+
+ await queueConsumer.StartAsync(CancellationToken.None);
+ await Task.Delay(10000);
+ await queueConsumer.StopAsync(CancellationToken.None);
+
+ // Assert
+ sendingServiceMock.Verify(s => s.SendAsync(It.IsAny()), Times.Never);
+ }
+
+ private SendSmsQueueConsumer GetSmsSendingConsumer(ISendingService sendingService)
+ {
+ IServiceCollection services = new ServiceCollection()
+ .AddLogging()
+ .AddSingleton(_kafkaSettings)
+ .AddSingleton()
+ .AddSingleton(sendingService)
+ .AddHostedService();
+
+ _serviceProvider = services.BuildServiceProvider();
+
+ var smsSendingConsumer = _serviceProvider.GetService(typeof(IHostedService)) as SendSmsQueueConsumer;
+
+ if (smsSendingConsumer == null)
+ {
+ Assert.Fail("Unable to create an instance of SmsSendingConsumer.");
+ }
+
+ return smsSendingConsumer;
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/Endpoints/HealthCheckTests.cs b/test/Altinn.Notifications.Sms.IntegrationTests/Endpoints/HealthCheckTests.cs
new file mode 100644
index 0000000..ffe44b1
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.IntegrationTests/Endpoints/HealthCheckTests.cs
@@ -0,0 +1,26 @@
+using Altinn.Notifications.Sms.Health;
+
+namespace Altinn.Notifications.Sms.IntegrationTests.Endpoints;
+
+public class HealthCheckTests : IClassFixture>
+{
+ private readonly IntegrationTestWebApplicationFactory _factory;
+
+ public HealthCheckTests(IntegrationTestWebApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task Health_Test()
+ {
+ // Arrange
+ HttpClient httpClient = _factory.CreateClient();
+
+ // Act
+ HttpResponseMessage actual = await httpClient.GetAsync("/health");
+
+ // Assert
+ Assert.Equal(200, (int)actual.StatusCode);
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/IntegrationTestWebApplicationFactory.cs b/test/Altinn.Notifications.Sms.IntegrationTests/IntegrationTestWebApplicationFactory.cs
new file mode 100644
index 0000000..e139455
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.IntegrationTests/IntegrationTestWebApplicationFactory.cs
@@ -0,0 +1,31 @@
+using Altinn.Notifications.Sms.Integrations.Consumers;
+
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+
+namespace Altinn.Notifications.Sms.IntegrationTests;
+
+public class IntegrationTestWebApplicationFactory : WebApplicationFactory
+ where TStartup : class
+{
+ ///
+ /// ConfigureWebHost for setup of configuration and test services
+ ///
+ /// IWebHostBuilder
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureAppConfiguration(config =>
+ {
+ config.AddConfiguration(new ConfigurationBuilder()
+ .Build());
+ });
+
+ builder.ConfigureTestServices(services =>
+ {
+ var descriptor = services.Single(s => s.ImplementationType == typeof(SendSmsQueueConsumer));
+ services.Remove(descriptor);
+ });
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/UnitTest1.cs b/test/Altinn.Notifications.Sms.IntegrationTests/UnitTest1.cs
deleted file mode 100644
index d47ec17..0000000
--- a/test/Altinn.Notifications.Sms.IntegrationTests/UnitTest1.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace Altinn.Notifications.Sms.IntegrationTests
-{
- ///
- /// Unit test
- ///
- public class UnitTest1
- {
- ///
- /// Test1
- ///
- [Fact]
- public void Test1()
- {
- string actual = "test";
-
- Assert.Equal("test", actual);
- }
- }
-}
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/Utils/KafkaUtil.cs b/test/Altinn.Notifications.Sms.IntegrationTests/Utils/KafkaUtil.cs
new file mode 100644
index 0000000..87e2dd8
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.IntegrationTests/Utils/KafkaUtil.cs
@@ -0,0 +1,48 @@
+using Altinn.Notifications.Sms.Core.Dependencies;
+using Altinn.Notifications.Sms.Integrations.Producers;
+
+using Confluent.Kafka;
+using Confluent.Kafka.Admin;
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Altinn.Notifications.Sms.IntegrationTests.Utils;
+
+public static class KafkaUtil
+{
+ private const string _brokerAddress = "localhost:9092";
+
+ public static async Task DeleteTopicAsync(string topic)
+ {
+ using var adminClient = new AdminClientBuilder(new Dictionary() { { "bootstrap.servers", _brokerAddress } }).Build();
+ await adminClient.DeleteTopicsAsync([topic]);
+ }
+
+ public static async Task CreateTopicsAsync(string topic)
+ {
+ using var adminClient = new AdminClientBuilder(new Dictionary() { { "bootstrap.servers", _brokerAddress } }).Build();
+
+ await adminClient.CreateTopicsAsync(
+ [
+ new TopicSpecification
+ {
+ Name = topic,
+ NumPartitions = 1, // Set the desired number of partitions
+ ReplicationFactor = 1 // Set the desired replication factor
+ }
+
+ ]);
+ }
+
+ public static CommonProducer GetKafkaProducer(ServiceProvider serviceProvider)
+ {
+ var kafkaProducer = serviceProvider.GetService(typeof(ICommonProducer)) as CommonProducer;
+
+ if (kafkaProducer == null)
+ {
+ Assert.Fail("Unable to create an instance of KafkaProducer.");
+ }
+
+ return kafkaProducer;
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.IntegrationTests/xunit.runner.json b/test/Altinn.Notifications.Sms.IntegrationTests/xunit.runner.json
new file mode 100644
index 0000000..015166b
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.IntegrationTests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false
+}
\ No newline at end of file
diff --git a/test/Altinn.Notifications.Sms.Tests/Altinn.Notifications.Sms.Tests.csproj b/test/Altinn.Notifications.Sms.Tests/Altinn.Notifications.Sms.Tests.csproj
index 1b836dd..6358105 100644
--- a/test/Altinn.Notifications.Sms.Tests/Altinn.Notifications.Sms.Tests.csproj
+++ b/test/Altinn.Notifications.Sms.Tests/Altinn.Notifications.Sms.Tests.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Core/Sending/SendingServiceTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Core/Sending/SendingServiceTests.cs
new file mode 100644
index 0000000..8a2ea3a
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Core/Sending/SendingServiceTests.cs
@@ -0,0 +1,74 @@
+using Altinn.Notifications.Sms.Core.Configuration;
+using Altinn.Notifications.Sms.Core.Dependencies;
+using Altinn.Notifications.Sms.Core.Sending;
+using Altinn.Notifications.Sms.Core.Status;
+using Moq;
+
+namespace Altinn.Notifications.Sms.Tests.Sms.Core.Sending;
+
+public class SendingServiceTests
+{
+ private readonly TopicSettings _topicSettings;
+
+ public SendingServiceTests()
+ {
+ _topicSettings = new()
+ {
+ SmsStatusUpdatedTopicName = "SmsStatusUpdatedTopicName"
+ };
+ }
+
+ [Fact]
+ public async Task SendAsync_GatewayReferenceGenerated_PublishedToExpectedKafkaTopic()
+ {
+ // Arrange
+ Guid id = Guid.NewGuid();
+ Notifications.Sms.Core.Sending.Sms sms = new(id, "recipient", "sender", "message");
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.SendAsync(It.IsAny()))
+ .ReturnsAsync("gateway-reference");
+
+ Mock producerMock = new();
+ producerMock.Setup(p => p.ProduceAsync(
+ It.Is(s => s.Equals(nameof(_topicSettings.SmsStatusUpdatedTopicName))),
+ It.Is(s =>
+ s.Contains("\"gatewayReference\":\"gateway-reference\"") &&
+ s.Contains($"\"notificationId\":\"{id}\""))));
+
+ var sut = new SendingService(clientMock.Object, producerMock.Object, _topicSettings);
+
+ // Act
+ await sut.SendAsync(sms);
+
+ // Assert
+ producerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task SendAsync_InvalidReceiver_PublishedToExpectedKafkaTopic()
+ {
+ // Arrange
+ Guid id = Guid.NewGuid();
+ Notifications.Sms.Core.Sending.Sms sms = new(id, "recipient", "sender", "message");
+
+ Mock clientMock = new();
+ clientMock.Setup(c => c.SendAsync(It.IsAny()))
+ .ReturnsAsync(new SmsClientErrorResponse { SendResult = SmsSendResult.Failed_InvalidReceiver, ErrorMessage = "Receiver is wrong" });
+
+ Mock producerMock = new();
+ producerMock.Setup(p => p.ProduceAsync(
+ It.Is(s => s.Equals(nameof(_topicSettings.SmsStatusUpdatedTopicName))),
+ It.Is(s =>
+ s.Contains($"\"notificationId\":\"{id}\"") &&
+ s.Contains("\"sendResult\":\"Failed_InvalidReceiver\""))));
+
+ var sut = new SendingService(clientMock.Object, producerMock.Object, _topicSettings);
+
+ // Act
+ await sut.SendAsync(sms);
+
+ // Assert
+ producerMock.VerifyAll();
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Core/Sending/SmsTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Core/Sending/SmsTests.cs
new file mode 100644
index 0000000..fca7ee6
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Core/Sending/SmsTests.cs
@@ -0,0 +1,55 @@
+using System.Text.Json.Nodes;
+
+namespace Altinn.Notifications.Sms.Tests.Sms.Core.Sending;
+
+public class SmsTests
+{
+ private readonly string _serializedSms;
+ private readonly Guid _id;
+
+ public SmsTests()
+ {
+ _id = Guid.NewGuid();
+ _serializedSms = new JsonObject()
+ {
+ { "notificationId", _id },
+ { "recipient", "recipient" },
+ { "sender", "sender" },
+ { "message", "message" }
+ }.ToJsonString();
+ }
+
+ [Fact]
+ public void TryParse_ValidSms_True()
+ {
+ bool actualResult = Notifications.Sms.Core.Sending.Sms.TryParse(_serializedSms, out Notifications.Sms.Core.Sending.Sms actual);
+
+ Assert.True(actualResult);
+ Assert.Equal(_id, actual.NotificationId);
+ Assert.Equal("message", actual.Message);
+ }
+
+ [Fact]
+ public void TryParse_EmptyString_False()
+ {
+ bool actualResult = Notifications.Sms.Core.Sending.Sms.TryParse(string.Empty, out _);
+
+ Assert.False(actualResult);
+ }
+
+ [Fact]
+ public void TryParse_InvalidString_False()
+ {
+ bool actualResult = Notifications.Sms.Core.Sending.Sms.TryParse("{\"notificationId\":\"nothing\"}", out _);
+
+ Assert.False(actualResult);
+ }
+
+ [Fact]
+ public void TryParse_InvalidJsonExceptionThrown_False()
+ {
+ bool actualResult = Notifications.Sms.Core.Sending.Sms.TryParse("{\"fakefield\":\"nothing\"}", out _);
+
+ Assert.False(actualResult);
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Core/ServiceCollectionExtensionsTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Core/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..0cb6d53
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Core/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,42 @@
+using Altinn.Notifications.Sms.Core.Configuration;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Altinn.Notifications.Sms.Tests.Sms.Core;
+
+public class ServiceCollectionExtensionsTests
+{
+ [Fact]
+ public void AddIntegrationServices_MissingKafkaConfig_ThrowsException()
+ {
+ // Arrange
+ string expectedExceptionMessage = "Required Kafka settings is missing from application configuration (Parameter 'config')";
+
+ var config = new ConfigurationBuilder().Build();
+
+ IServiceCollection services = new ServiceCollection();
+
+ // Act
+ var exception = Assert.Throws(() => services.AddCoreServices(config));
+
+ // Assert
+ Assert.Equal(expectedExceptionMessage, exception.Message);
+ }
+
+ [Fact]
+ public void AddIntegrationServices_SmsGatewayConfigIncluded_NoException()
+ {
+ // Arrange
+ Environment.SetEnvironmentVariable("KafkaSettings__BrokerAddress", "localhost:9092", EnvironmentVariableTarget.Process);
+ var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
+
+ IServiceCollection services = new ServiceCollection();
+
+ // Act
+ var exception = Record.Exception(() => services.AddCoreServices(config));
+
+ // Assert
+ Assert.Null(exception);
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Core/Status/SendOperationResultTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Core/Status/SendOperationResultTests.cs
new file mode 100644
index 0000000..e9c9fa9
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Core/Status/SendOperationResultTests.cs
@@ -0,0 +1,42 @@
+using System.Text.Json.Nodes;
+
+using Altinn.Notifications.Sms.Core.Status;
+
+namespace Altinn.Notifications.Sms.Tests.Sms.Core.Status;
+
+public class SendOperationResultTests
+{
+ private readonly SendOperationResult _operationResult;
+ private readonly string _serializedOperationResult;
+
+ public SendOperationResultTests()
+ {
+ Guid id = Guid.NewGuid();
+ _operationResult = new SendOperationResult()
+ {
+ NotificationId = id,
+ GatewayReference = "gateway-reference",
+ SendResult = SmsSendResult.Accepted
+ };
+
+ _serializedOperationResult = new JsonObject()
+ {
+ { "notificationId", id },
+ { "gatewayReference", "gateway-reference" },
+ { "sendResult", "Accepted" }
+ }.ToJsonString();
+ }
+
+ [Fact]
+ public void SerializeToJson()
+ {
+ // Arrange
+ string expected = _serializedOperationResult;
+
+ // Act
+ var actual = _operationResult.Serialize();
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/ServiceCollectionExtensionsTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/ServiceCollectionExtensionsTests.cs
index f33ff5d..ce5dc96 100644
--- a/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/ServiceCollectionExtensionsTests.cs
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/ServiceCollectionExtensionsTests.cs
@@ -3,7 +3,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-namespace Altinn.Notifications.Tests.Notifications.Integrations.TestingExtensions;
+namespace Altinn.Notifications.Sms.Tests.Sms.Integrations;
public class ServiceCollectionExtensionsTests
{
@@ -11,9 +11,29 @@ public class ServiceCollectionExtensionsTests
public void AddIntegrationServices_MissingSmsGatewayConfig_ThrowsException()
{
// Arrange
+ Environment.SetEnvironmentVariable("KafkaSettings__BrokerAddress", "localhost:9092", EnvironmentVariableTarget.Process);
string expectedExceptionMessage = "Required SmsGatewayConfiguration settings is missing from application configuration. (Parameter 'config')";
- var config = new ConfigurationBuilder().Build();
+ var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
+
+ IServiceCollection services = new ServiceCollection();
+
+ // Act
+ var exception = Assert.Throws(() => services.AddIntegrationServices(config));
+
+ // Assert
+ Assert.Equal(expectedExceptionMessage, exception.Message);
+ }
+
+ [Fact]
+ public void AddIntegrationServices_MissingKafkaConfig_ThrowsException()
+ {
+ // Arrange
+ Environment.SetEnvironmentVariable("KafkaSettings__BrokerAddress", null, EnvironmentVariableTarget.Process);
+ Environment.SetEnvironmentVariable("SmsGatewayConfiguration__Endpoint", "https://vg.no", EnvironmentVariableTarget.Process);
+ string expectedExceptionMessage = "Required Kafka settings is missing from application configuration (Parameter 'config')";
+
+ var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
IServiceCollection services = new ServiceCollection();
@@ -29,6 +49,7 @@ public void AddIntegrationServices_SmsGatewayConfigIncluded_NoException()
{
// Arrange
Environment.SetEnvironmentVariable("SmsGatewayConfiguration__Endpoint", "https://vg.no", EnvironmentVariableTarget.Process);
+ Environment.SetEnvironmentVariable("KafkaSettings__BrokerAddress", "localhost:9092", EnvironmentVariableTarget.Process);
var config = new ConfigurationBuilder().AddEnvironmentVariables().Build();
IServiceCollection services = new ServiceCollection();
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SharedClientConfigTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SharedClientConfigTests.cs
new file mode 100644
index 0000000..7882d60
--- /dev/null
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SharedClientConfigTests.cs
@@ -0,0 +1,43 @@
+using Altinn.Notifications.Sms.Integrations.Configuration;
+
+using Xunit;
+
+namespace Altinn.Notifications.Sms.Tests.Sms.Integrations;
+
+public class SharedClientConfigTests
+{
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void SharedClientConfig_ParamsSetBySASLProperties(bool includeUsernameAndPassword)
+ {
+ // Arrange
+ KafkaSettings settings = new()
+ {
+ BrokerAddress = "localhost:9092"
+ };
+
+ if (includeUsernameAndPassword)
+ {
+ settings.Admin.SaslUsername = "username";
+ settings.Admin.SaslPassword = "password";
+ }
+
+ // Act
+ var config = new SharedClientConfig(settings);
+
+ // Assert
+ if (includeUsernameAndPassword)
+ {
+ Assert.Equal(3, config.TopicSpecification.ReplicationFactor);
+ Assert.NotNull(config.AdminClientConfig.SaslMechanism);
+ Assert.Equal("password", config.AdminClientConfig.SaslPassword);
+ }
+ else
+ {
+ Assert.Null(config.AdminClientConfig.SaslMechanism);
+ Assert.True(string.IsNullOrEmpty(config.ProducerConfig.SaslUsername));
+ Assert.Equal(1, config.TopicSpecification.NumPartitions);
+ }
+ }
+}
diff --git a/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SmsClientTests.cs b/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SmsClientTests.cs
index 8276821..a59e2ee 100644
--- a/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SmsClientTests.cs
+++ b/test/Altinn.Notifications.Sms.Tests/Sms.Integrations/SmsClientTests.cs
@@ -1,4 +1,5 @@
using Altinn.Notifications.Sms.Core.Status;
+
using Altinn.Notifications.Sms.Integrations.LinkMobility;
using LinkMobility.PSWin.Client.Model;
@@ -25,7 +26,7 @@ public async Task SendAsync_GatewayReturnsNonSuccess_UnknownError()
SmsClient smsClient = new(_clientMock.Object);
// Act
- var result = await smsClient.SendAsync(new Core.Sending.Sms());
+ var result = await smsClient.SendAsync(new Notifications.Sms.Core.Sending.Sms());
// Assert
Assert.True(result.IsError);
@@ -54,7 +55,7 @@ public async Task SendAsync_GatewayReturnsNonSuccess_InvalidReceiver()
SmsClient smsClient = new(_clientMock.Object);
// Act
- var result = await smsClient.SendAsync(new Core.Sending.Sms());
+ var result = await smsClient.SendAsync(new Notifications.Sms.Core.Sending.Sms());
// Assert
Assert.True(result.IsError);
@@ -86,7 +87,7 @@ public async Task SendAsync_GatewayReturnsSuccess_GatewayRefReturned()
SmsClient smsClient = new(_clientMock.Object);
// Act
- var result = await smsClient.SendAsync(new Core.Sending.Sms());
+ var result = await smsClient.SendAsync(new Notifications.Sms.Core.Sending.Sms());
// Assert
Assert.True(result.IsSuccess);
diff --git a/test/setup-kafka.yml b/test/setup-kafka.yml
new file mode 100644
index 0000000..d01b4e5
--- /dev/null
+++ b/test/setup-kafka.yml
@@ -0,0 +1,38 @@
+---
+version: '3'
+services:
+ kafdrop:
+ image: obsidiandynamics/kafdrop
+ restart: always
+ ports:
+ - "9000:9000"
+ environment:
+ KAFKA_BROKERCONNECT: "broker:29092"
+ JVM_OPTS: "-Xms16M -Xmx48M -XX:-TieredCompilation -XX:+UseStringDeduplication -noverify"
+ depends_on:
+ - "broker"
+
+ zookeeper:
+ image: confluentinc/cp-zookeeper:7.0.1
+ container_name: zookeeper
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ZOOKEEPER_TICK_TIME: 2000
+ restart: always
+
+ broker:
+ image: confluentinc/cp-kafka:7.0.1
+ container_name: broker
+ ports:
+ - "9092:9092"
+ depends_on:
+ - zookeeper
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
+ restart: always