Skip to content

Commit 8dfd8f6

Browse files
committed
refactor SocketIO by TDD
1 parent 9177f3a commit 8dfd8f6

File tree

7 files changed

+202
-41
lines changed

7 files changed

+202
-41
lines changed

src/SocketIO.Serializer.SystemTextJson/SocketIO.Serializer.SystemTextJson.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</ItemGroup>
2020

2121
<ItemGroup>
22-
<PackageReference Include="System.Text.Json" Version="8.0.4" />
22+
<PackageReference Include="System.Text.Json" Version="9.0.4" />
2323
</ItemGroup>
2424

2525
</Project>

src/SocketIOClient/V2/SocketIO.cs

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,10 @@ namespace SocketIOClient.V2;
1616

1717
public class SocketIO : ISocketIO
1818
{
19-
public IHttpClient HttpClient { get; set; }
20-
public ISessionFactory SessionFactory { get; set; }
21-
private ISession _session;
22-
public int PacketId { get; private set; }
23-
public bool Connected { get; private set; }
24-
25-
26-
private readonly Dictionary<int, Action<IAckMessage>> _ackHandlers = new();
27-
private readonly Dictionary<int, Func<IAckMessage, Task>> _funcHandlers = new();
28-
private readonly SocketIOOptions _options;
29-
30-
3119
public SocketIO(Uri uri, SocketIOOptions options)
3220
{
33-
_options = options;
21+
_serverUri = uri;
22+
Options = options;
3423
SessionFactory = new DefaultSessionFactory();
3524
}
3625

@@ -42,14 +31,66 @@ public SocketIO(Uri uri, SocketIOOptions options)
4231
{
4332
}
4433

45-
public Task ConnectAsync()
34+
public IHttpClient HttpClient { get; set; }
35+
public ISessionFactory SessionFactory { get; set; }
36+
private ISession _session;
37+
public int PacketId { get; private set; }
38+
public bool Connected { get; private set; }
39+
public string Id { get; private set; }
40+
41+
public string Namespace { get; private set; }
42+
43+
private Uri _serverUri;
44+
45+
private Uri ServerUri
4646
{
47-
_session = SessionFactory.New(_options.EIO);
48-
// Session.Subscribe(this);
49-
Connected = true;
50-
return Task.CompletedTask;
47+
get => _serverUri;
48+
set
49+
{
50+
if (_serverUri != value)
51+
{
52+
_serverUri = value;
53+
if (value != null && value.AbsolutePath != "/")
54+
{
55+
Namespace = value.AbsolutePath;
56+
}
57+
}
58+
}
5159
}
5260

61+
62+
private readonly Dictionary<int, Action<IAckMessage>> _ackHandlers = new();
63+
private readonly Dictionary<int, Func<IAckMessage, Task>> _funcHandlers = new();
64+
public SocketIOOptions Options { get; }
65+
public event EventHandler<Exception> OnReconnectError;
66+
67+
public async Task ConnectAsync()
68+
{
69+
var attempts = Options.Reconnection ? Options.ReconnectionAttempts : 1;
70+
for (int i = 0; i < attempts; i++)
71+
{
72+
// TODO: IDisposable
73+
var session = SessionFactory.New(Options.EIO);
74+
using var cts = new CancellationTokenSource(Options.ConnectionTimeout);
75+
try
76+
{
77+
await session.ConnectAsync(cts.Token);
78+
_session = session;
79+
_session.Subscribe(this);
80+
}
81+
catch (Exception e)
82+
{
83+
var ex = new ConnectionException($"Cannot connect to server '{ServerUri}'", e);
84+
OnReconnectError?.Invoke(this, ex);
85+
if (i == attempts - 1)
86+
{
87+
throw ex;
88+
}
89+
}
90+
}
91+
}
92+
93+
5394
// public Task EmitAsync(string eventName, Action ack)
5495
// {
5596
// throw new NotImplementedException();
@@ -82,17 +123,35 @@ public async Task EmitAsync(string eventName, Func<IAckMessage, Task> ack)
82123

83124
public async Task OnNextAsync(IMessage message)
84125
{
85-
if (message.Type == MessageType.Ack)
126+
switch (message.Type)
86127
{
87-
var ackMessage = (IAckMessage)message;
88-
if (_ackHandlers.TryGetValue(ackMessage.Id, out var ack))
89-
{
90-
ack(ackMessage);
91-
}
92-
else if (_funcHandlers.TryGetValue(ackMessage.Id, out var func))
93-
{
94-
await func(ackMessage);
95-
}
128+
case MessageType.Connected:
129+
await HandleConnectedMessage(message);
130+
break;
131+
case MessageType.Ack:
132+
await HandleAckMessage(message);
133+
break;
96134
}
97135
}
136+
137+
private async Task HandleAckMessage(IMessage message)
138+
{
139+
var ackMessage = (IAckMessage)message;
140+
if (_ackHandlers.TryGetValue(ackMessage.Id, out var ack))
141+
{
142+
ack(ackMessage);
143+
}
144+
else if (_funcHandlers.TryGetValue(ackMessage.Id, out var func))
145+
{
146+
await func(ackMessage);
147+
}
148+
}
149+
150+
private Task HandleConnectedMessage(IMessage message)
151+
{
152+
var connectedMessage = (ConnectedMessage)message;
153+
Id = connectedMessage.Sid;
154+
Connected = true;
155+
return Task.CompletedTask;
156+
}
98157
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
using System;
2+
13
namespace SocketIOClient.V2;
24

35
public class SocketIOOptions
46
{
57
public EngineIO EIO { get; set; }
8+
public TimeSpan ConnectionTimeout { get; set; }
9+
public bool Reconnection { get; set; }
10+
public int ReconnectionAttempts { get; set; }
611
}

tests/SocketIOClient.IntegrationTests/SocketIOClient.IntegrationTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<ItemGroup>
88
<PackageReference Include="FluentAssertions" Version="6.12.0" />
99
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
10-
<PackageReference Include="Moq" Version="4.20.70" />
10+
<PackageReference Include="Moq" Version="4.20.72" />
1111
<PackageReference Include="MSTest.TestAdapter" Version="3.2.2" />
1212
<PackageReference Include="MSTest.TestFramework" Version="3.2.2" />
1313
<PackageReference Include="coverlet.collector" Version="6.0.2">

tests/SocketIOClient.Serializer.Tests/SocketIOClient.Serializer.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<ItemGroup>
1414
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
1515
<PackageReference Include="FluentAssertions" Version="6.12.0" />
16-
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
16+
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
1717
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
1818
<PackageReference Include="xunit" Version="2.5.3"/>
1919
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>

tests/SocketIOClient.UnitTests/SocketIOClient.UnitTests.csproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
57

68
<IsPackable>false</IsPackable>
7-
<AssemblyOriginatorKeyFile>../../src/SocketIOClient/SocketIOClient.snk</AssemblyOriginatorKeyFile>
8-
<SignAssembly>true</SignAssembly>
9+
<IsTestProject>true</IsTestProject>
910
<LangVersion>default</LangVersion>
10-
<Nullable>enable</Nullable>
1111
</PropertyGroup>
1212

1313
<ItemGroup>
@@ -29,6 +29,10 @@
2929
</PackageReference>
3030
</ItemGroup>
3131

32+
<ItemGroup>
33+
<Using Include="Xunit"/>
34+
</ItemGroup>
35+
3236
<ItemGroup>
3337
<ProjectReference Include="..\..\src\SocketIOClient\SocketIOClient.csproj" />
3438
</ItemGroup>

tests/SocketIOClient.UnitTests/V2/SocketIOTests.cs

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
using System;
2-
using System.Threading.Tasks;
31
using FluentAssertions;
42
using NSubstitute;
3+
using NSubstitute.ExceptionExtensions;
4+
using SocketIOClient.Core.Messages;
55
using SocketIOClient.V2;
66
using SocketIOClient.V2.Serializer.SystemTextJson;
77
using SocketIOClient.V2.Session;
8-
using Xunit;
98

109
namespace SocketIOClient.UnitTests.V2;
1110

@@ -25,6 +24,17 @@ public SocketIOTests()
2524
private readonly SocketIOClient.V2.SocketIO _io;
2625
private readonly ISession _session;
2726

27+
[Fact]
28+
public void NothingCalled_DefaultValues()
29+
{
30+
var io = new SocketIOClient.V2.SocketIO("http://localhost:3000");
31+
io.PacketId.Should().Be(0);
32+
io.Connected.Should().BeFalse();
33+
io.Id.Should().BeNull();
34+
io.Namespace.Should().BeNull();
35+
io.SessionFactory.Should().BeOfType<DefaultSessionFactory>();
36+
}
37+
2838
[Fact]
2939
public async Task EmitAsync_NotConnected_ThrowException()
3040
{
@@ -35,9 +45,92 @@ await _io.Invoking(x => x.EmitAsync("event", _ => { }))
3545
}
3646

3747
[Fact]
38-
public async Task EmitAsync_AckEventAction_PacketIdIncrementBy1()
48+
public async Task ConnectAsync_FailedToConnect_ThrowConnectionException()
49+
{
50+
_io.Options.Reconnection = false;
51+
_io.Options.ReconnectionAttempts = 1;
52+
_session.ConnectAsync(Arg.Any<CancellationToken>()).ThrowsAsync(new Exception("Test"));
53+
54+
await _io
55+
.Invoking(async x => await x.ConnectAsync())
56+
.Should()
57+
.ThrowAsync<ConnectionException>()
58+
.WithMessage("Cannot connect to server 'http://localhost:3000/'");
59+
}
60+
61+
[Fact]
62+
public async Task ConnectAsync_ReconnectionIsFalseAttempsIs2_OnReconnectErrorInvoked1Time()
63+
{
64+
_io.Options.Reconnection = false;
65+
_io.Options.ReconnectionAttempts = 2;
66+
_session.ConnectAsync(Arg.Any<CancellationToken>()).ThrowsAsync(new Exception("Test"));
67+
68+
var times = 0;
69+
_io.OnReconnectError += (_, _) => times++;
70+
71+
await _io
72+
.Invoking(async x => await x.ConnectAsync())
73+
.Should()
74+
.ThrowAsync<ConnectionException>();
75+
76+
times.Should().Be(1);
77+
}
78+
79+
[Theory]
80+
[InlineData(1)]
81+
[InlineData(5)]
82+
public async Task ConnectAsync_FailedToConnect_OnReconnectErrorInvokedByAttempts(int attempts)
83+
{
84+
_io.Options.Reconnection = true;
85+
_io.Options.ReconnectionAttempts = attempts;
86+
_session.ConnectAsync(Arg.Any<CancellationToken>()).ThrowsAsync(new Exception("Test"));
87+
88+
var times = 0;
89+
_io.OnReconnectError += (_, _) => times++;
90+
91+
await _io
92+
.Invoking(async x => await x.ConnectAsync())
93+
.Should()
94+
.ThrowAsync<ConnectionException>();
95+
96+
times.Should().Be(attempts);
97+
}
98+
99+
[Fact]
100+
public async Task ConnectAsync_SessionSuccessfullyConnected_SessionSubscribeIO()
101+
{
102+
await _io.ConnectAsync();
103+
_session.Received(1).Subscribe(_io);
104+
}
105+
106+
[Fact]
107+
public async Task ConnectAsync_SessionSuccessfullyConnectedButNoConnectedMessage_ConnectedIsFalse()
39108
{
40109
await _io.ConnectAsync();
110+
_io.Connected.Should().BeFalse();
111+
}
112+
113+
private async Task ConnectAsync()
114+
{
115+
await _io.ConnectAsync();
116+
await _io.OnNextAsync(new ConnectedMessage
117+
{
118+
Sid = "123",
119+
});
120+
}
121+
122+
[Fact]
123+
public async Task ConnectAsync_ConnectedMessageReceived_ConnectedIsTrueIdHasValue()
124+
{
125+
await ConnectAsync();
126+
_io.Connected.Should().BeTrue();
127+
_io.Id.Should().Be("123");
128+
}
129+
130+
[Fact]
131+
public async Task EmitAsync_AckEventAction_PacketIdIncrementBy1()
132+
{
133+
await ConnectAsync();
41134
await _io.EmitAsync("event", _ => { });
42135

43136
_io.PacketId.Should().Be(1);
@@ -48,7 +141,7 @@ public async Task EmitAsync_AckEventActionAndGotResponse_HandlerIsCalled()
48141
{
49142
var ackCalled = false;
50143

51-
await _io.ConnectAsync();
144+
await ConnectAsync();
52145
await _io.EmitAsync("event", _ => ackCalled = true);
53146
var ackMessage = new SystemJsonAckMessage
54147
{
@@ -62,7 +155,7 @@ public async Task EmitAsync_AckEventActionAndGotResponse_HandlerIsCalled()
62155
[Fact]
63156
public async Task EmitAsync_AckEventFunc_PacketIdIncrementBy1()
64157
{
65-
await _io.ConnectAsync();
158+
await ConnectAsync();
66159
await _io.EmitAsync("event", _ => Task.CompletedTask);
67160

68161
_io.PacketId.Should().Be(1);
@@ -73,7 +166,7 @@ public async Task EmitAsync_AckEventFuncAndGotResponse_HandlerIsCalled()
73166
{
74167
var ackCalled = false;
75168

76-
await _io.ConnectAsync();
169+
await ConnectAsync();
77170
await _io.EmitAsync("event", _ =>
78171
{
79172
ackCalled = true;

0 commit comments

Comments
 (0)