Skip to content

Commit d5dcba5

Browse files
committed
💡 Add SMPP client library and test project
Introduced a new SMPP client library to manage SMS operations, including connection handling, message submission, and delivery receipt processing. Added unit tests to validate core functionality, ensuring reliability and maintaining expected behaviors.
1 parent df9af23 commit d5dcba5

12 files changed

+476
-0
lines changed

Client.Tests/Client.Tests.csproj

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<RootNamespace>Elyfe.Smpp.Client.Tests</RootNamespace>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="coverlet.collector" Version="6.0.2" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
14+
<PackageReference Include="Moq" Version="4.20.72" />
15+
<PackageReference Include="xunit" Version="2.9.2" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<Using Include="Xunit" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\Client\Client.csproj" />
25+
</ItemGroup>
26+
27+
</Project>

Client.Tests/SmscClientTests.cs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using JamaaTech.Smpp.Net.Lib;
2+
using JamaaTech.Smpp.Net.Lib.Protocol;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
5+
using Moq;
6+
7+
namespace Elyfe.Smpp.Client.Tests;
8+
9+
public class SmscClientTests
10+
{
11+
private readonly Mock<ILogger<SmscClient>> _mockLogger;
12+
private readonly IOptions<SmscOptions> _options;
13+
private readonly Mock<SmppClientSession> _mockSession;
14+
private readonly SmppEncodingService _encodingService;
15+
16+
private class TestableSmscClient : SmscClient
17+
{
18+
private readonly Mock<SmppClientSession> _mockSession;
19+
20+
public TestableSmscClient(
21+
ILogger<SmscClient> logger,
22+
IOptions<SmscOptions> options,
23+
Mock<SmppClientSession> mockSession) : base(logger, options)
24+
{
25+
_mockSession = mockSession;
26+
}
27+
28+
protected override SmppClientSession CreateAndBindSession(SessionBindInfo bindInfo, SmppEncodingService encodingService)
29+
{
30+
return _mockSession.Object;
31+
}
32+
}
33+
34+
public SmscClientTests()
35+
{
36+
_mockLogger = new Mock<ILogger<SmscClient>>();
37+
_encodingService = new SmppEncodingService();
38+
_options = Options.Create(new SmscOptions
39+
{
40+
Host = "localhost",
41+
Port = 2775,
42+
SystemId = "test",
43+
Password = "test",
44+
Reconnect = false
45+
});
46+
47+
_mockSession = new Mock<SmppClientSession>(_encodingService);
48+
}
49+
50+
[Fact]
51+
public void InitialState_ShouldBeClosed()
52+
{
53+
var client = new TestableSmscClient(_mockLogger.Object, _options, _mockSession);
54+
Assert.Equal(SmppConnectionState.Closed, client.ConnectionState);
55+
}
56+
57+
[Fact]
58+
public async Task ConnectAsync_WhenClosed_ShouldTransitionToBound()
59+
{
60+
var client = new TestableSmscClient(_mockLogger.Object, _options, _mockSession);
61+
var states = new List<SmppConnectionState>();
62+
client.ConnectionStateChanged += (_, e) => states.Add(e.NewState);
63+
64+
_mockSession.Setup(s => s.State).Returns(SmppSessionState.Open);
65+
66+
await client.ConnectAsync();
67+
68+
Assert.Equal(SmppConnectionState.Bound, client.ConnectionState);
69+
Assert.Equal(new[] { SmppConnectionState.Connecting, SmppConnectionState.Bound }, states);
70+
}
71+
72+
[Fact]
73+
public async Task DisconnectAsync_WhenBound_ShouldTransitionToClosed()
74+
{
75+
var client = new TestableSmscClient(_mockLogger.Object, _options, _mockSession);
76+
77+
_mockSession.Setup(s => s.State).Returns(SmppSessionState.Open);
78+
await client.ConnectAsync();
79+
80+
var states = new List<SmppConnectionState>();
81+
client.ConnectionStateChanged += (_, e) => states.Add(e.NewState);
82+
83+
await client.DisconnectAsync();
84+
_mockSession.Raise(s => s.SessionClosed += null, new SmppSessionClosedEventArgs(SmppSessionCloseReason.EndSessionCalled, null));
85+
86+
Assert.Equal(SmppConnectionState.Closed, client.ConnectionState);
87+
Assert.Contains(SmppConnectionState.Unbinding, states);
88+
Assert.Contains(SmppConnectionState.Closed, states);
89+
}
90+
91+
[Fact]
92+
public async Task SendSmsAsync_WhenNotBound_ShouldThrowInvalidOperationException()
93+
{
94+
var client = new TestableSmscClient(_mockLogger.Object, _options, _mockSession);
95+
var pdu = new SubmitSm(_encodingService);
96+
97+
await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendSingleSmsAsync(pdu));
98+
}
99+
100+
[Fact]
101+
public async Task SendSmsAsync_WhenBound_ShouldCallSessionSendPdu()
102+
{
103+
var client = new TestableSmscClient(_mockLogger.Object, _options, _mockSession);
104+
_mockSession.Setup(s => s.State).Returns(SmppSessionState.Open);
105+
await client.ConnectAsync();
106+
107+
var pdu = new SubmitSm(_encodingService);
108+
var expectedResponse = pdu.CreateDefaultResponse();
109+
_mockSession.Setup(s => s.SendPdu(pdu)).Returns(expectedResponse);
110+
111+
var response = await client.SendSingleSmsAsync(pdu);
112+
113+
_mockSession.Verify(s => s.SendPdu(pdu), Times.Once);
114+
Assert.Same(expectedResponse, response);
115+
}
116+
117+
[Fact]
118+
public async Task OnPduReceived_WithDeliverSm_ShouldRaiseDeliverSmReceivedEvent()
119+
{
120+
var client = new TestableSmscClient(_mockLogger.Object, _options, _mockSession);
121+
_mockSession.Setup(s => s.State).Returns(SmppSessionState.Open);
122+
await client.ConnectAsync();
123+
124+
DeliverSmEventArgs? receivedArgs = null;
125+
client.DeliverSmReceived += (_, e) => { receivedArgs = e; };
126+
127+
var deliverSmPdu = new DeliverSm(_encodingService);
128+
var pduReceivedEventArgs = new PduReceivedEventArgs(deliverSmPdu);
129+
130+
_mockSession.Raise(s => s.PduReceived += null, pduReceivedEventArgs);
131+
132+
Assert.NotNull(receivedArgs);
133+
Assert.Same(deliverSmPdu, receivedArgs.Pdu);
134+
}
135+
}

Client/Client.csproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>Elyfe.Smpp.Client</RootNamespace>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\JamaaTech.SMPP.Net.Lib\Smpp.Net.Lib.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
16+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
17+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.6" />
18+
</ItemGroup>
19+
20+
</Project>

Client/DeliverSmEventArgs.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using JamaaTech.Smpp.Net.Lib.Protocol;
2+
using System;
3+
4+
namespace Elyfe.Smpp.Client;
5+
6+
public class DeliverSmEventArgs : EventArgs
7+
{
8+
public DeliverSm Pdu { get; }
9+
10+
public DeliverSmEventArgs(DeliverSm pdu)
11+
{
12+
Pdu = pdu;
13+
}
14+
}

Client/ISmscClient.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using JamaaTech.Smpp.Net.Lib.Protocol;
2+
using System;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace Elyfe.Smpp.Client;
7+
8+
public interface ISmscClient : IDisposable
9+
{
10+
SmppConnectionState ConnectionState { get; }
11+
event EventHandler<SmppConnectionStateChangedEventArgs> ConnectionStateChanged;
12+
event EventHandler<DeliverSmEventArgs> DeliverSmReceived;
13+
Task ConnectAsync(CancellationToken cancellationToken = default);
14+
Task DisconnectAsync(CancellationToken cancellationToken = default);
15+
Task<SubmitSmResp> SendSingleSmsAsync(SubmitSm pdu, CancellationToken cancellationToken = default);
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace Elyfe.Smpp.Client;
5+
6+
public static class ServiceCollectionExtensions
7+
{
8+
public static IServiceCollection AddSmscClient(this IServiceCollection services, IConfiguration configuration)
9+
{
10+
services.Configure<SmscOptions>(configuration.GetSection("Smsc"));
11+
services.AddSingleton<ISmscClient, SmscClient>();
12+
return services;
13+
}
14+
}

Client/SmppConnectionState.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Elyfe.Smpp.Client;
2+
3+
public enum SmppConnectionState
4+
{
5+
Closed,
6+
Connecting,
7+
Connected,
8+
Binding,
9+
Bound,
10+
Unbinding
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace Elyfe.Smpp.Client;
4+
5+
public class SmppConnectionStateChangedEventArgs : EventArgs
6+
{
7+
public SmppConnectionState NewState { get; }
8+
public SmppConnectionState OldState { get; }
9+
10+
public SmppConnectionStateChangedEventArgs(SmppConnectionState newState, SmppConnectionState oldState)
11+
{
12+
NewState = newState;
13+
OldState = oldState;
14+
}
15+
}

0 commit comments

Comments
 (0)