refactor: reorganize solution for better multi-project support/management

This commit is contained in:
Stevan Freeborn
2025-05-25 18:38:52 -05:00
parent 00158dff04
commit a8de49cee3
111 changed files with 250 additions and 59 deletions
+9
View File
@@ -0,0 +1,9 @@
[*.cs]
dotnet_diagnostic.CA1515.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA2201.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1303.severity = none
dotnet_diagnostic.CA1031.severity = none
dotnet_diagnostic.CA1034.severity = none
dotnet_diagnostic.CA1054.severity = none
+15
View File
@@ -0,0 +1,15 @@
<Project>
<Import
Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
</ItemGroup>
</Project>
+19
View File
@@ -0,0 +1,19 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
<PackageVersion Include="FluentAssertions" Version="7.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageVersion Include="moq" Version="4.20.72" />
<PackageVersion Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="moq" />
<PackageReference Include="RichardSzalay.MockHttp" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StevesBot.Webhook\StevesBot.Webhook.csproj" />
</ItemGroup>
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[StevesBot.Webhook]*</Include>
<ExcludeByFile>**/Program.cs,**/Worker.cs</ExcludeByFile>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
<Exec Command="reportgenerator -reports:./TestResults/Coverage/*.xml -targetdir:./TestResults/Coverage/Report/ -reporttypes:Html_Dark" />
</Target>
</Project>
@@ -0,0 +1,10 @@
namespace StevesBot.Webhook.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
@@ -0,0 +1,117 @@
using WebSocket = System.Net.WebSockets.WebSocket;
namespace StevesBot.Worker.Tests.Integration.Infrastructure;
public sealed class TestWebSocketServer : IAsyncLifetime, IDisposable
{
private readonly IWebHost _host;
private readonly CancellationTokenSource _echoCts = new();
private Uri? _webSocketUri;
public Uri Uri
{
get
{
_webSocketUri ??= GetWebSocketUri();
return _webSocketUri;
}
}
public TestWebSocketServer()
{
_host = new WebHostBuilder()
.Configure(app =>
{
app.UseWebSockets();
app.Run(async context =>
{
if (context.Request.Path == "/ws" && context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(webSocket, _echoCts.Token);
}
else
{
context.Response.StatusCode = 400;
}
});
})
.UseKestrel()
.UseUrls("http://[::1]:0")
.Build();
}
public async Task InitializeAsync()
{
await _host.StartAsync();
}
public async Task DisposeAsync()
{
await _echoCts.CancelAsync();
await _host.StopAsync();
_echoCts.Dispose();
_host.Dispose();
}
private Uri GetWebSocketUri()
{
var address = _host.ServerFeatures.GetRequiredFeature<IServerAddressesFeature>().Addresses.First();
var webSocketAddress = address.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
return new Uri(webSocketAddress + "/ws");
}
private static async Task Echo(WebSocket webSocket, CancellationToken cancellationToken)
{
var buffer = new byte[1024 * 4];
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
while (result.CloseStatus.HasValue is false && cancellationToken.IsCancellationRequested is false)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
}
if (result.CloseStatus.HasValue && cancellationToken.IsCancellationRequested is false)
{
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
Console.WriteLine($"{nameof(TestWebSocketServer)}: Client connection closed prematurely.");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{nameof(TestWebSocketServer)}: Echo operation cancelled (server shutting down).");
if (webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived)
{
await webSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server shutting down", CancellationToken.None);
}
}
catch (Exception ex)
{
Console.WriteLine($"{nameof(TestWebSocketServer)}: Error during echo: {ex.Message}");
if (webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived)
{
await webSocket.CloseAsync(WebSocketCloseStatus.InternalServerError, "Server error", CancellationToken.None);
}
}
finally
{
webSocket.Dispose();
}
}
public void Dispose()
{
_host.Dispose();
}
}
@@ -0,0 +1,41 @@
using WebSocket = StevesBot.Worker.WebSockets.WebSocket;
namespace StevesBot.Worker.Tests.Integration;
public sealed class WebSocketTests : IClassFixture<TestWebSocketServer>, IDisposable
{
private readonly WebSocket _client = new();
private readonly TestWebSocketServer _server;
public WebSocketTests(TestWebSocketServer server)
{
_server = server;
}
[Fact]
public async Task WebSocket_WhenUsed_ItShouldProperlyConnectSendMsgReceiveMsgAndClose()
{
await _client.ConnectAsync(_server.Uri, CancellationToken.None);
var message = "Hello, WebSocket!";
var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message));
await _client.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
var receivedBuffer = new ArraySegment<byte>(new byte[buffer.Count]);
var receivedMessage = await _client.ReceiveAsync(receivedBuffer, CancellationToken.None);
receivedMessage.MessageType.Should().Be(WebSocketMessageType.Text);
receivedMessage.EndOfMessage.Should().BeTrue();
receivedMessage.Count.Should().Be(buffer.Count);
var receivedString = Encoding.UTF8.GetString([.. receivedBuffer], 0, receivedMessage.Count);
receivedString.Should().Be(message);
await _client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", CancellationToken.None);
}
public void Dispose()
{
_client.Dispose();
}
}
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="moq" />
<PackageReference Include="RichardSzalay.MockHttp" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[StevesBot.Worker]*</Include>
<ExcludeByFile>**/Program.cs,**/Worker.cs</ExcludeByFile>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
<Exec
Command="reportgenerator -reports:./TestResults/Coverage/*.xml -targetdir:./TestResults/Coverage/Report/ -reporttypes:Html_Dark" />
</Target>
<ItemGroup>
<ProjectReference Include="..\..\src\StevesBot.Worker\StevesBot.Worker.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,14 @@
namespace StevesBot.Worker.Tests.Unit;
public class ActivityTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldCreateAnInstance()
{
var activity = new Activity();
activity.Name.Should().Be(string.Empty);
activity.Type.Should().Be(ActivityType.Custom);
activity.State.Should().BeNull();
}
}
@@ -0,0 +1,19 @@
namespace StevesBot.Worker.Tests.Unit;
public class ActivityTypeTests
{
[Theory]
[MemberData(nameof(TestData))]
public void ActivityType_WhenCalled_ItShouldReturnExpectedResult(int activityType, int expected)
{
activityType.Should().Be(expected);
}
public static TheoryData<int, int> TestData => new()
{
{
ActivityType.Custom,
4
},
};
}
@@ -0,0 +1,199 @@
namespace StevesBot.Worker.Tests.Unit;
public class AsyncLockTests
{
[Fact]
public async Task LockAsync_WhenCalled_ItShouldReturnsNonNullRelease()
{
using var asyncLock = new AsyncLock();
var releaser = await asyncLock.LockAsync();
releaser.Should().NotBeNull();
releaser.Should().BeAssignableTo<IDisposable>();
}
[Fact]
public async Task LockAsync_WhenLockIsAcquired_ItShouldForceSubsequentCallsToWait()
{
using var asyncLock = new AsyncLock();
var lockOneAcquired = new TaskCompletionSource<bool>();
var lockTwoAttempted = new TaskCompletionSource<bool>();
var lockTwoAcquired = new TaskCompletionSource<bool>();
async Task FirstLockAction()
{
using (await asyncLock.LockAsync())
{
lockOneAcquired.SetResult(true);
await lockTwoAttempted.Task;
await Task.Delay(100);
}
}
async Task SecondLockAction()
{
await lockOneAcquired.Task;
lockTwoAttempted.SetResult(true);
using (await asyncLock.LockAsync())
{
lockTwoAcquired.SetResult(true);
}
}
var taskOne = FirstLockAction();
var taskTwo = SecondLockAction();
await Task.WhenAll(taskOne, Task.WhenAny(taskTwo, Task.Delay(500)));
lockOneAcquired.Task.IsCompleted.Should().BeTrue();
lockTwoAttempted.Task.IsCompleted.Should().BeTrue();
lockTwoAcquired.Task.IsCompleted.Should().BeTrue();
}
[Fact]
public async Task LockAsync_WhenReleaserIsDisposed_ItShouldAllowAnotherLockAcquisition()
{
using var asyncLock = new AsyncLock();
var releaser = await asyncLock.LockAsync();
releaser.Dispose();
IDisposable? newReleaser = null;
var act = async () => newReleaser = await asyncLock.LockAsync();
await act.Should().NotThrowAsync();
newReleaser.Should().NotBeNull();
newReleaser.Dispose();
}
[Fact]
public async Task LockAsync_WhenCalledWithCancellationTokenAndTokenIsCancelled_ItShouldThrowOperationCanceledException()
{
using var asyncLock = new AsyncLock();
using var cts = new CancellationTokenSource();
var initialReleaser = await asyncLock.LockAsync();
async Task<IDisposable> Act()
{
return await asyncLock.LockAsync(cts.Token);
}
var lockTask = Act();
await Task.Delay(100);
await cts.CancelAsync();
var act = () => lockTask;
await act.Should().ThrowAsync<OperationCanceledException>();
initialReleaser.Dispose();
}
[Fact]
public async Task LockAsync_WhenCalledWithAlreadyCancelledToken_ItShouldThrowOperationCanceledException()
{
using var asyncLock = new AsyncLock();
using var cts = new CancellationTokenSource();
await cts.CancelAsync();
var act = async () => await asyncLock.LockAsync(cts.Token);
await act.Should().ThrowAsync<OperationCanceledException>();
}
[Fact]
public async Task Dispose_WhenCalled_ItShouldDisposeSemaphore()
{
var asyncLock = new AsyncLock();
asyncLock.Dispose();
var act = () => asyncLock.LockAsync();
await act.Should().ThrowAsync<ObjectDisposedException>();
}
[Fact]
public void Dispose_WhenCalledMultipleTimes_ItShouldNotThrow()
{
using var asyncLock = new AsyncLock();
var act = () =>
{
asyncLock.Dispose();
asyncLock.Dispose();
};
act.Should().NotThrow();
}
[Fact]
public async Task LockAsync_WhenCalledAfterDispose_ItShouldThrowObjectDisposedException()
{
var asyncLock = new AsyncLock();
asyncLock.Dispose();
var act = () => asyncLock.LockAsync();
await act.Should().ThrowAsync<ObjectDisposedException>();
}
[Fact]
public async Task LockAsync_WhenCalled_ItShouldOnlyAllowConcurrentAccessToOneThread()
{
using var asyncLock = new AsyncLock();
var concurrentAccessCount = 0;
var maxConcurrentAccessCount = 0;
const int numberOfTasks = 10;
var tasks = new Task[numberOfTasks];
for (var i = 0; i < numberOfTasks; i++)
{
tasks[i] = Task.Run(async () =>
{
using (await asyncLock.LockAsync())
{
Interlocked.Increment(ref concurrentAccessCount);
maxConcurrentAccessCount = Math.Max(maxConcurrentAccessCount, concurrentAccessCount);
await Task.Delay(20);
Interlocked.Decrement(ref concurrentAccessCount);
}
});
}
await Task.WhenAll(tasks);
maxConcurrentAccessCount.Should().Be(1);
concurrentAccessCount.Should().Be(0);
}
[Fact]
public async Task LockAsync_WhenCalled_ItShouldPreventRaceConditionsInHighContentionScenario()
{
using var asyncLock = new AsyncLock();
var sharedResource = 0;
const int numberOfIterations = 10;
const int numberOfTasks = 10;
var tasks = new Task[numberOfTasks];
for (var i = 0; i < numberOfTasks; i++)
{
tasks[i] = Task.Run(async () =>
{
for (var j = 0; j < numberOfIterations; j++)
{
using (await asyncLock.LockAsync())
{
var temp = sharedResource;
await Task.Delay(1);
sharedResource = temp + 1;
}
}
});
}
await Task.WhenAll(tasks);
sharedResource.Should().Be(numberOfTasks * numberOfIterations);
}
}
@@ -0,0 +1,22 @@
namespace StevesBot.Worker.Tests.Unit;
public class CreateMessageRequestTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var content = "content";
var messageReference = new DiscordMessageReference(
1,
"message_id",
"channel_id",
"guild_id",
false
);
var result = new CreateMessageRequest(content, messageReference);
result.Content.Should().Be(content);
result.MessageReference.Should().BeSameAs(messageReference);
}
}
@@ -0,0 +1,37 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordClientOptionsTests
{
[Fact]
public void Constructor_WhenCalledWithoutParameters_ItShouldCreateInstance()
{
var options = new DiscordClientOptions();
options.Should().NotBeNull();
options.Should().BeOfType<DiscordClientOptions>();
options.ApiUrl.Should().Be(string.Empty);
options.AppToken.Should().Be(string.Empty);
options.Intents.Should().Be(0);
}
[Fact]
public void Constructor_WhenCalledWithParameters_ItShouldCreateInstance()
{
var apiUrl = "https://api.example.com";
var appToken = "test-token";
var intents = 123;
var options = new DiscordClientOptions
{
ApiUrl = apiUrl,
AppToken = appToken,
Intents = intents
};
options.Should().NotBeNull();
options.Should().BeOfType<DiscordClientOptions>();
options.ApiUrl.Should().Be(apiUrl);
options.AppToken.Should().Be(appToken);
options.Intents.Should().Be(intents);
}
}
@@ -0,0 +1,20 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordCloseCodesTests
{
[Theory]
[InlineData(4004, false)]
[InlineData(4010, false)]
[InlineData(4011, false)]
[InlineData(4012, false)]
[InlineData(4013, false)]
[InlineData(4014, false)]
[InlineData(null, true)]
[InlineData(1000, true)]
public void IsReconnectable_WhenCalledWithCloseCode_ItShouldReturnExpectedResult(int? closeCode, bool expected)
{
var result = DiscordCloseCodes.IsReconnectable(closeCode);
result.Should().Be(expected);
}
}
@@ -0,0 +1,145 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordEventConverterTests
{
private readonly JsonSerializerOptions _options = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new DiscordEventConverter()
}
};
private readonly Type _discordEventType = typeof(DiscordEvent);
private readonly DiscordEventConverter _converter = new();
[Theory]
[MemberData(nameof(TestData))]
public void Read_WhenCalledWithOpCode_ItShouldReturnDiscordEvent(object data, Type expectedType)
{
var result = Read(data);
result.Should().BeOfType(expectedType);
}
[Fact]
public void Write_WhenCalledWithDiscordEvent_ItShouldReturnJson()
{
var discordEvent = new DiscordEvent
{
OpCode = DiscordOpCodes.Dispatch,
Sequence = null,
Type = null,
Data = null
};
var result = JsonSerializer.Serialize(discordEvent, _options);
var expectedJson = /*lang=json,strict*/ "{\"op\":0,\"s\":null,\"t\":null,\"d\":null}";
result.Should().Be(expectedJson);
}
private DiscordEvent? Read(object data)
{
var json = JsonSerializer.Serialize(data);
var utf8Json = Encoding.UTF8.GetBytes(json);
var reader = new Utf8JsonReader(utf8Json);
return _converter.Read(ref reader, _discordEventType, _options);
}
public static TheoryData<object, Type> TestData => new()
{
{
new
{
op = DiscordOpCodes.Hello,
s = null as int?,
t = null as string,
d = null as object,
},
typeof(HelloDiscordEvent)
},
{
new
{
op = DiscordOpCodes.Dispatch,
s = null as int?,
t = null as string,
d = null as object
},
typeof(DispatchDiscordEvent)
},
{
new
{
op = DiscordOpCodes.Dispatch,
s = null as int?,
t = DiscordEventTypes.Ready,
d = null as object
},
typeof(ReadyDiscordEvent)
},
{
new
{
op = DiscordOpCodes.Dispatch,
s = null as int?,
t = DiscordEventTypes.MessageCreate,
d = null as object
},
typeof(MessageCreateDiscordEvent)
},
{
new
{
op = DiscordOpCodes.HeartbeatAck,
s = null as int?,
t = null as string,
d = null as object
},
typeof(HeartbeatAckDiscordEvent)
},
{
new
{
op = -1,
s = null as int?,
t = null as string,
d = null as object
},
typeof(DiscordEvent)
},
{
new
{
op = 1,
s = null as int?,
t = null as string,
d = null as object
},
typeof(HeartbeatDiscordEvent)
},
{
new
{
op = 7,
s = null as int?,
t = null as string,
d = null as object
},
typeof(ReconnectDiscordEvent)
},
{
new
{
op = 9,
s = null as int?,
t = null as string,
d = false
},
typeof(InvalidSessionDiscordEvent)
}
};
}
@@ -0,0 +1,41 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldCreateInstance()
{
var e = new DiscordEvent();
e.Should().NotBeNull();
e.Should().BeOfType<DiscordEvent>();
e.OpCode.Should().Be(0);
e.Sequence.Should().BeNull();
e.Type.Should().BeNull();
e.Data.Should().BeNull();
}
[Fact]
public void Constructor_WhenCalledWithParameters_ItShouldCreateInstance()
{
var opCode = 1;
var sequence = 2;
var type = "test";
var data = new object();
var e = new DiscordEvent
{
OpCode = opCode,
Sequence = sequence,
Type = type,
Data = data,
};
e.Should().NotBeNull();
e.Should().BeOfType<DiscordEvent>();
e.OpCode.Should().Be(opCode);
e.Sequence.Should().Be(sequence);
e.Type.Should().Be(type);
e.Data.Should().BeSameAs(data);
}
}
@@ -0,0 +1,38 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordEventTypesTests
{
[Theory]
[MemberData(nameof(TestData))]
public void Event_WhenCalled_ItShouldReturnCorrectValue(string eventType, string expectedValue)
{
eventType.Should().Be(expectedValue);
}
public static TheoryData<string, string> TestData => new()
{
{ DiscordEventTypes.Ready, "READY" },
{ DiscordEventTypes.GuildMemberAdd, "GUILD_MEMBER_ADD" },
{ DiscordEventTypes.MessageCreate, "MESSAGE_CREATE" },
};
[Theory]
[MemberData(nameof(IsValidEventNameTestData))]
public void IsValidEventName_WhenCalled_ItShouldReturnCorrectValue(string? eventName, bool expected)
{
var result = DiscordEventTypes.IsValidEvent(eventName!);
result.Should().Be(expected);
}
public static TheoryData<string?, bool> IsValidEventNameTestData => new()
{
{ " ", false },
{ "", false },
{ null, false },
{ "I MADE IT UP", false },
{ DiscordEventTypes.Ready, true },
{ DiscordEventTypes.GuildMemberAdd, true },
{ DiscordEventTypes.MessageCreate, true },
};
}
@@ -0,0 +1,37 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordGatewayClientExceptionTests
{
[Fact]
public void Constructor_WhenCalledWithoutParameters_ItShouldCreateInstance()
{
var exception = new DiscordGatewayClientException();
exception.Should().NotBeNull();
exception.Should().BeOfType<DiscordGatewayClientException>();
}
[Fact]
public void Constructor_WhenCalledWithMessage_ItShouldCreateInstance()
{
var message = "Test message";
var exception = new DiscordGatewayClientException(message);
exception.Should().NotBeNull();
exception.Should().BeOfType<DiscordGatewayClientException>();
exception.Message.Should().Be(message);
}
[Fact]
public void Constructor_WhenCalledWithMessageAndInnerException_ItShouldCreateInstance()
{
var message = "Test message";
var innerException = new Exception("Inner exception");
var exception = new DiscordGatewayClientException(message, innerException);
exception.Should().NotBeNull();
exception.Should().BeOfType<DiscordGatewayClientException>();
exception.Message.Should().Be(message);
exception.InnerException.Should().Be(innerException);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordIntentsTests
{
[Theory]
[MemberData(nameof(TestData))]
public void Intents_ShouldHaveCorrectValue(long intent, long expectedValue)
{
intent.Should().Be(expectedValue);
}
public static TheoryData<long, long> TestData => new()
{
{ DiscordIntents.Guilds, 1 },
{ DiscordIntents.GuildMembers, 2 },
{ DiscordIntents.GuildModeration, 4 },
{ DiscordIntents.GuildExpressions, 8 },
{ DiscordIntents.GuildIntegrations, 16 },
{ DiscordIntents.GuildWebhooks, 32 },
{ DiscordIntents.GuildInvites, 64 },
{ DiscordIntents.GuildVoiceStates, 128 },
{ DiscordIntents.GuildPresences, 256 },
{ DiscordIntents.GuildMessages, 512 },
{ DiscordIntents.GuildMessageReactions, 1024 },
{ DiscordIntents.GuildMessageTyping, 2048 },
{ DiscordIntents.DirectMessages, 4096 },
{ DiscordIntents.DirectMessageReactions, 8192 },
{ DiscordIntents.DirectMessageTyping, 16384 },
{ DiscordIntents.MessageContent, 32768 },
{ DiscordIntents.GuildScheduledEvents, 65536 },
{ DiscordIntents.AutoModerationConfiguration, 1048576 },
{ DiscordIntents.AutoModerationExecution, 2097152 },
{ DiscordIntents.GuildMessagePolls, 16777216 },
{ DiscordIntents.DirectMessagePolls, 33554432 },
{ DiscordIntents.All, 53608447 }
};
}
@@ -0,0 +1,28 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordMessageReferenceTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var type = 1;
var messageId = "message_id";
var channelId = "channel_id";
var guildId = "guild_id";
var failIfNotExists = true;
var result = new DiscordMessageReference(
type,
messageId,
channelId,
guildId,
failIfNotExists
);
result.Type.Should().Be(type);
result.MessageId.Should().Be(messageId);
result.ChannelId.Should().Be(channelId);
result.GuildId.Should().Be(guildId);
result.FailIfNotExists.Should().Be(failIfNotExists);
}
}
@@ -0,0 +1,17 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordMessageReferenceTypesTests
{
[Theory]
[MemberData(nameof(TestData))]
public void Type_WhenCalled_ItShouldReturnExpectedValue(int type, int expected)
{
type.Should().Be(expected);
}
public static TheoryData<int, int> TestData => new()
{
{ DiscordMessageReferenceTypes.Default, 0 },
{ DiscordMessageReferenceTypes.Forward, 1 },
};
}
@@ -0,0 +1,19 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordOpCodesTests
{
[Theory]
[MemberData(nameof(TestData))]
public void OpCode_ShouldHaveCorrectValue(int opCode, int expectedValue)
{
opCode.Should().Be(expectedValue);
}
public static TheoryData<int, int> TestData => new()
{
{ DiscordOpCodes.Dispatch, 0 },
{ DiscordOpCodes.Heartbeat, 1 },
{ DiscordOpCodes.Hello, 10 },
{ DiscordOpCodes.HeartbeatAck, 11 }
};
}
@@ -0,0 +1,34 @@
namespace StevesBot.Worker.Tests.Unit;
public class DiscordRestClientExceptionTests
{
[Fact]
public void Constructor_WhenCalledWithNoParameters_ItShouldCreateInstance()
{
var exception = new DiscordRestClientException();
exception.Should().NotBeNull();
}
[Fact]
public void Constructor_WhenCalledWithMessage_ItShouldCreateInstance()
{
var message = "Test message";
var exception = new DiscordRestClientException(message);
exception.Should().NotBeNull();
exception.Message.Should().Be(message);
}
[Fact]
public void Constructor_WhenCalledWithMessageAndInnerException_ItShouldCreateInstance()
{
var message = "Test message";
var innerException = new Exception("Inner exception");
var exception = new DiscordRestClientException(message, innerException);
exception.Should().NotBeNull();
exception.Message.Should().Be(message);
exception.InnerException.Should().Be(innerException);
}
}
@@ -0,0 +1,138 @@
using Microsoft.AspNetCore.Http.HttpResults;
namespace StevesBot.Worker.Tests.Unit;
public sealed class DiscordRestClientTests : IDisposable
{
private const string BaseUrl = "https://discord.com/api/v10";
private static string GatewayEndpoint => $"{BaseUrl}/gateway";
private static string ChannelMessagesEndpoint => $"{BaseUrl}/channels/*/messages";
private readonly Mock<ILogger<DiscordRestClient>> _mockLogger = new();
private readonly MockHttpMessageHandler _mockHttpMessageHandler;
private readonly DiscordRestClient _discordRestClient;
public DiscordRestClientTests()
{
_mockHttpMessageHandler = new MockHttpMessageHandler();
var httpClient = _mockHttpMessageHandler.ToHttpClient();
httpClient.BaseAddress = new Uri("https://discord.com/api/v10/");
_discordRestClient = new DiscordRestClient(_mockLogger.Object, httpClient);
}
[Fact]
public void Constructor_WhenLoggerIsNull_ItShouldThrowAnException()
{
var act = () => new DiscordRestClient(null!, _mockHttpMessageHandler.ToHttpClient());
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenHttpClientIsNull_ItShouldThrowAnException()
{
var act = () => new DiscordRestClient(_mockLogger.Object, null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public async Task GetGatewayUrlAsync_WhenRequestFails_ItShouldThrowException()
{
_mockHttpMessageHandler
.When(GatewayEndpoint)
.Respond(HttpStatusCode.InternalServerError);
var act = async () => await _discordRestClient.GetGatewayUrlAsync(CancellationToken.None);
await act.Should().ThrowAsync<DiscordRestClientException>();
}
[Fact]
public async Task GetGatewayUrlAsync_WhenResponseIsNull_ItShouldThrowException()
{
_mockHttpMessageHandler
.When(GatewayEndpoint)
.Respond(HttpStatusCode.OK, "application/json", "null");
var act = async () => await _discordRestClient.GetGatewayUrlAsync(CancellationToken.None);
await act.Should().ThrowAsync<DiscordRestClientException>();
}
[Fact]
public async Task GetGatewayUrlAsync_WhenRequestIsSuccessful_ItShouldReturnGatewayUrl()
{
var expectedUrl = "test";
var jsonResponse = $@"{{
""url"": ""{expectedUrl}""
}}";
_mockHttpMessageHandler
.When(GatewayEndpoint)
.Respond(HttpStatusCode.OK, "application/json", jsonResponse);
var result = await _discordRestClient.GetGatewayUrlAsync(CancellationToken.None);
result.Should().Be(expectedUrl);
}
[Fact]
public async Task CreateMessageAsync_WhenRequestFails_ItShouldThrowAnException()
{
_mockHttpMessageHandler
.When(ChannelMessagesEndpoint)
.Respond(HttpStatusCode.InternalServerError);
var request = new CreateMessageRequest(
"content",
new(DiscordMessageReferenceTypes.Default, "message_id", "channel_id", "guild_id", false)
);
var act = async () => await _discordRestClient.CreateMessageAsync("channel_id", request);
await act.Should().ThrowAsync<DiscordRestClientException>();
}
[Fact]
public async Task CreateMessageyAsync_WhenResponseIsNull_ItShouldThrowAnException()
{
_mockHttpMessageHandler
.When(ChannelMessagesEndpoint)
.Respond(HttpStatusCode.OK, "application/json", "null");
var request = new CreateMessageRequest(
"content",
new(DiscordMessageReferenceTypes.Default, "message_id", "channel_id", "guild_id", false)
);
var act = async () => await _discordRestClient.CreateMessageAsync("channel_id", request);
await act.Should().ThrowAsync<DiscordRestClientException>();
}
[Fact]
public async Task CreateMessageAsync_WhenRequestSucceeds_ItShouldReturnMessage()
{
var message = new DiscordMessage();
var messageResponse = JsonSerializer.Serialize(message);
_mockHttpMessageHandler
.When(ChannelMessagesEndpoint)
.Respond(HttpStatusCode.OK, "application/json", messageResponse);
var request = new CreateMessageRequest(
"content",
new(DiscordMessageReferenceTypes.Default, "message_id", "channel_id", "guild_id", false)
);
var result = await _discordRestClient.CreateMessageAsync("channel_id", request);
result.Should().BeEquivalentTo(message);
}
public void Dispose()
{
_mockHttpMessageHandler.Dispose();
}
}
@@ -0,0 +1,70 @@
namespace StevesBot.Worker.Tests.Unit;
public class ExtensionsTests
{
private readonly DiscordClientOptions _discordClientOptions = new()
{
ApiUrl = "https://test.com",
AppToken = "test_token",
};
private readonly Mock<IDiscordRestClient> _mockDiscordRestClient = new();
private readonly Mock<ILogger<DiscordGatewayClient>> _mockLogger = new();
private readonly Mock<IWebSocketFactory> _mockWebSocketFactory = new();
private readonly Mock<TimeProvider> _mockTimeProvider = new();
private readonly Mock<IServiceScopeFactory> _mockScopeFactory = new();
private readonly ServiceCollection _services = new();
public ExtensionsTests()
{
_services.AddSingleton(_discordClientOptions);
_services.AddSingleton(_mockLogger.Object);
_services.AddSingleton(_mockWebSocketFactory.Object);
_services.AddSingleton(_mockTimeProvider.Object);
_services.AddSingleton(_mockScopeFactory.Object);
}
[Fact]
public void AddDiscordRestClient_WhenCalled_ItShouldAddDiscordRestClient()
{
_services.AddDiscordRestClient();
var act = () => _services
.BuildServiceProvider()
.GetRequiredService<IDiscordRestClient>();
act.Should().NotThrow();
}
[Fact]
public void AddDiscordGatewayClient_WhenCalled_ItShouldAddDiscordGatewayClient()
{
_services.AddSingleton(_mockDiscordRestClient.Object);
_services.AddDiscordGatewayClient();
var act = () => _services
.BuildServiceProvider()
.GetRequiredService<IDiscordGatewayClient>();
act.Should().NotThrow();
}
[Fact]
public void AddDiscordGatewayClient_WhenCalledWithConfigureAction_ItShouldAddDiscordGatewayClientAndConfigureIt()
{
var mockAction = new Mock<Action<IDiscordGatewayClient>>();
_services.AddSingleton(_mockDiscordRestClient.Object);
_services.AddDiscordGatewayClient(mockAction.Object);
var act = () => _services
.BuildServiceProvider()
.GetRequiredService<IDiscordGatewayClient>();
act.Should().NotThrow();
mockAction.Invocations.Count.Should().Be(1);
}
}
@@ -0,0 +1,15 @@
namespace StevesBot.Worker.Tests.Unit;
public class HeartbeatAckDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var result = new HeartbeatAckDiscordEvent();
result.OpCode.Should().Be(DiscordOpCodes.HeartbeatAck);
result.Sequence.Should().BeNull();
result.Type.Should().BeNull();
result.Data.Should().BeNull();
}
}
@@ -0,0 +1,17 @@
namespace StevesBot.Worker.Tests.Unit;
public class HeartbeatDiscordEventTests
{
[Theory]
[InlineData(null)]
[InlineData(1)]
public void Constructor_WhenCalled_ItShouldInitializeProperties(int? sequence)
{
var heartbeatEvent = new HeartbeatDiscordEvent(sequence);
heartbeatEvent.OpCode.Should().Be(1);
heartbeatEvent.Sequence.Should().Be(sequence);
heartbeatEvent.Type.Should().BeNull();
heartbeatEvent.Data.Should().BeNull();
}
}
@@ -0,0 +1,71 @@
namespace StevesBot.Worker.Tests.Unit;
public class HelloDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var helloEvent = new HelloDiscordEvent();
var helloData = new HelloData();
helloEvent.OpCode.Should().Be(DiscordOpCodes.Hello);
helloEvent.Sequence.Should().BeNull();
helloEvent.Type.Should().BeNull();
helloEvent.Data.Should().BeEquivalentTo(helloData);
helloData.HeartbeatInterval.Should().Be(0);
}
[Fact]
public void Constructor_WhenCalledWithParameters_ItShouldInitializeProperties()
{
var opCode = 1;
var sequence = 2;
var type = "type";
var heartbeatInterval = 1000;
var helloData = new HelloData
{
HeartbeatInterval = heartbeatInterval
};
var helloEvent = new HelloDiscordEvent
{
OpCode = opCode,
Sequence = sequence,
Type = type,
Data = helloData
};
helloEvent.OpCode.Should().Be(opCode);
helloEvent.Sequence.Should().Be(sequence);
helloEvent.Type.Should().Be(type);
helloEvent.Data.Should().BeSameAs(helloData);
}
[Fact]
public void Deserialize_WhenCalled_ItShouldReturnHelloDiscordEvent()
{
var data = new
{
op = 0,
s = null as int?,
t = null as string,
d = new
{
heartbeat_interval = 1000
}
};
var json = JsonSerializer.Serialize(data);
var helloEvent = JsonSerializer.Deserialize<HelloDiscordEvent>(json);
helloEvent.Should().NotBeNull();
helloEvent!.OpCode.Should().Be(data.op);
helloEvent.Sequence.Should().Be(data.s);
helloEvent.Type.Should().Be(data.t);
helloEvent.Data.Should().NotBeNull();
helloEvent.Data!.HeartbeatInterval.Should().Be(data.d.heartbeat_interval);
}
}
@@ -0,0 +1,50 @@
using StevesBot.Worker.Discord.Gateway.Events.Data;
namespace StevesBot.Worker.Tests.Unit;
public class IdentifyDataTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnInstance()
{
var result = new IdentifyData();
result.Token.Should().Be(string.Empty);
result.Properties.Should().BeEquivalentTo(new IdentifyProperties());
result.Presence.Should().BeEquivalentTo(new UpdatePresenceData());
result.Intents.Should().Be(0);
}
[Fact]
public void Constructor_WhenCalledWithParameters_ItShouldReturnInstance()
{
var token = "test_token";
var properties = new IdentifyProperties
{
Os = "test_os",
Browser = "test_browser",
Device = "test_device",
};
var presence = new UpdatePresenceData
{
Status = "test_status",
Activities = [],
};
var intents = 123456789;
var result = new IdentifyData
{
Token = token,
Properties = properties,
Presence = presence,
Intents = intents
};
result.Token.Should().Be(token);
result.Properties.Should().BeSameAs(properties);
result.Presence.Should().BeSameAs(presence);
result.Intents.Should().Be(intents);
}
}
@@ -0,0 +1,23 @@
namespace StevesBot.Worker.Tests.Unit;
public class IdentifyDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnInstance()
{
var token = "test_token";
var intents = 123456789;
var presence = new UpdatePresenceData
{
Status = "online",
Activities = [],
};
var result = new IdentifyDiscordEvent(token, intents, presence);
result.OpCode.Should().Be(DiscordOpCodes.Identify);
result.Data.Token.Should().Be(token);
result.Data.Intents.Should().Be(intents);
result.Data.Presence.Should().BeSameAs(presence);
}
}
@@ -0,0 +1,15 @@
namespace StevesBot.Worker.Tests.Unit;
public class InvalidSessionDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var result = new InvalidSessionDiscordEvent();
result.OpCode.Should().Be(DiscordOpCodes.InvalidSession);
result.Type.Should().BeNull();
result.Sequence.Should().BeNull();
result.Data.Should().BeFalse();
}
}
@@ -0,0 +1,60 @@
namespace StevesBot.Worker.Tests.Unit;
public class LockReleaserTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldCreateInstance()
{
using var semaphore = new SemaphoreSlim(1);
using var lockReleaser = new LockReleaser(semaphore);
lockReleaser.Should().NotBeNull();
lockReleaser.Should().BeOfType<LockReleaser>();
}
[Fact]
public void Constructor_WhenCalledAndSemaphoreIsNull_ItShouldThrowArgumentNullException()
{
var act = static () => new LockReleaser(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Dispose_WhenCalled_ItShouldReleaseSemaphore()
{
using var semaphore = new SemaphoreSlim(0, 1);
using var lockReleaser = new LockReleaser(semaphore);
lockReleaser.Dispose();
semaphore.CurrentCount.Should().Be(1);
}
[Fact]
public void Dispose_WhenCalledMultipleTimes_ItShouldReleaseSemaphoreOnlyOnce()
{
using var semaphore = new SemaphoreSlim(0, 1);
using var lockReleaser = new LockReleaser(semaphore);
lockReleaser.Dispose();
lockReleaser.Dispose();
semaphore.CurrentCount.Should().Be(1);
}
[Fact]
public void Dispose_WhenCalledAndSemaphoreIsAlreadyDisposed_ItShouldNotThrowException()
{
var act = () =>
{
var semaphore = new SemaphoreSlim(0, 1);
var lockReleaser = new LockReleaser(semaphore);
semaphore.Dispose();
lockReleaser.Dispose();
};
act.Should().NotThrow();
}
}
@@ -0,0 +1,30 @@
namespace StevesBot.Worker.Tests.Unit;
public class MessageCreateDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var result = new MessageCreateDiscordEvent();
result.OpCode.Should().Be(0);
result.Type.Should().Be(DiscordEventTypes.MessageCreate);
result.Sequence.Should().BeNull();
result.Data.Should().BeEquivalentTo(new DiscordMessage());
}
[Theory]
[InlineData(0, 0, true)]
[InlineData(1, 0, false)]
public void IsMessageType_WhenCalled_ItShouldReturnCorrectResult(int messageType, int givenType, bool expectedResult)
{
var e = new MessageCreateDiscordEvent()
{
Data = new() { Type = messageType }
};
var result = e.IsMessageType(givenType);
result.Should().Be(expectedResult);
}
}
@@ -0,0 +1,23 @@
namespace StevesBot.Worker.Tests.Unit;
public class PresenceStatusTests
{
[Theory]
[MemberData(nameof(TestData))]
public void PresenceStatus_WhenCalled_ItShouldReturnExpectedResult(string status, string expected)
{
status.Should().Be(expected);
}
public static TheoryData<string, string> TestData => new()
{
{
PresenceStatus.Online,
"online"
},
{
PresenceStatus.Idle,
"idle"
},
};
}
@@ -0,0 +1,72 @@
namespace StevesBot.Worker.Tests.Unit;
public class ReadyDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var readyEvent = new ReadyDiscordEvent();
var readyData = new ReadyData();
readyEvent.OpCode.Should().Be(0);
readyEvent.Sequence.Should().BeNull();
readyEvent.Type.Should().Be(DiscordEventTypes.Ready);
readyEvent.Data.Should().BeEquivalentTo(readyData);
readyData.Version.Should().Be(0);
readyData.SessionId.Should().Be(string.Empty);
readyData.ResumeGatewayUrl.Should().Be(string.Empty);
}
[Fact]
public void Constructor_WhenCalledWithParameters_ItShouldInitializeProperties()
{
var readyData = new ReadyData
{
Version = 1,
SessionId = "session_id",
ResumeGatewayUrl = "resume_gateway_url"
};
var readyEvent = new ReadyDiscordEvent
{
OpCode = 1,
Sequence = 2,
Type = "type",
Data = readyData
};
readyEvent.OpCode.Should().Be(1);
readyEvent.Sequence.Should().Be(2);
readyEvent.Type.Should().Be("type");
readyEvent.Data.Should().BeSameAs(readyData);
}
[Fact]
public void Deserialize_WhenCalled_ItShouldReturnReadyDiscordEvent()
{
var data = new
{
op = 0,
s = null as int?,
t = null as string,
d = new
{
v = 1,
session_id = "session_id",
resume_gateway_url = "resume_gateway_url"
}
};
var json = JsonSerializer.Serialize(data);
var readyEvent = JsonSerializer.Deserialize<ReadyDiscordEvent>(json);
readyEvent.Should().NotBeNull();
readyEvent!.OpCode.Should().Be(data.op);
readyEvent.Sequence.Should().Be(data.s);
readyEvent.Type.Should().Be(data.t);
readyEvent.Data.Version.Should().Be(data.d.v);
readyEvent.Data.SessionId.Should().Be(data.d.session_id);
readyEvent.Data.ResumeGatewayUrl.Should().Be(data.d.resume_gateway_url);
}
}
@@ -0,0 +1,15 @@
namespace StevesBot.Worker.Tests.Unit;
public class ReconnectDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var result = new ReconnectDiscordEvent();
result.OpCode.Should().Be(DiscordOpCodes.Reconnect);
result.Sequence.Should().BeNull();
result.Type.Should().BeNull();
result.Data.Should().BeNull();
}
}
@@ -0,0 +1,14 @@
namespace StevesBot.Worker.Tests.Unit;
public class ResumeDataTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var result = new ResumeData();
result.Token.Should().BeEmpty();
result.SessionId.Should().BeEmpty();
result.Sequence.Should().Be(0);
}
}
@@ -0,0 +1,24 @@
namespace StevesBot.Worker.Tests.Unit;
public class ResumeDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnAnInstance()
{
var token = "test_token";
var sessionId = "session_id";
var sequence = 1;
var result = new ResumeDiscordEvent(token, sessionId, sequence);
result.OpCode.Should().Be(DiscordOpCodes.Resume);
result.Type.Should().BeNull();
result.Sequence.Should().BeNull();
result.Data.Should().BeEquivalentTo(new ResumeData()
{
Token = token,
SessionId = sessionId,
Sequence = sequence,
});
}
}
@@ -0,0 +1,37 @@
namespace StevesBot.Worker.Tests.Unit;
public class UpdatePresenceDataTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldCreateAnInstance()
{
var updatePresenceData = new UpdatePresenceData();
updatePresenceData.Since.Should().BeNull();
updatePresenceData.Activities.Should().BeEmpty();
updatePresenceData.Status.Should().Be(PresenceStatus.Online);
updatePresenceData.Afk.Should().BeFalse();
}
[Fact]
public void Constructor_WhenCalledAndPropertiesAreInitialized_ItShouldCreateAnInstance()
{
var since = 1;
var activities = new List<Activity>();
var status = "some_made_up_status";
var afk = true;
var updatePresenceData = new UpdatePresenceData()
{
Since = since,
Activities = activities,
Status = status,
Afk = afk,
};
updatePresenceData.Since.Should().Be(since);
updatePresenceData.Activities.Should().BeSameAs(activities);
updatePresenceData.Status.Should().Be(status);
updatePresenceData.Afk.Should().Be(afk);
}
}
@@ -0,0 +1,20 @@
namespace StevesBot.Worker.Tests.Unit;
public class UpdatePresenceDiscordEventTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldReturnInstance()
{
var activities = new List<Activity>();
var since = 1234567890L;
var e = new UpdatePresenceDiscordEvent(since, activities, PresenceStatus.Online, false);
e.Should().NotBeNull();
e.Data.Should().NotBeNull();
e.Data.Since.Should().Be(since);
e.Data.Activities.Should().BeSameAs(activities);
e.Data.Status.Should().Be(PresenceStatus.Online);
e.Data.Afk.Should().BeFalse();
}
}
@@ -0,0 +1,17 @@
using WebSocket = StevesBot.Worker.WebSockets.WebSocket;
namespace StevesBot.Worker.Tests.Unit;
public class WebSocketFactoryTests
{
private readonly WebSocketFactory _webSocketFactory = new();
[Fact]
public void Create_WhenCalled_ItShouldReturnNewWebSocketInstance()
{
var result = _webSocketFactory.Create();
result.Should().NotBeNull();
result.Should().BeOfType<WebSocket>();
}
}
@@ -0,0 +1,15 @@
using WebSocket = StevesBot.Worker.WebSockets.WebSocket;
namespace StevesBot.Worker.Tests.Unit;
public class WebSocketTests
{
[Fact]
public void State_WhenCalled_ItShouldReturnWebSocketState()
{
using var webSocket = new WebSocket();
var state = webSocket.State;
state.Should().Be(WebSocketState.None);
}
}
@@ -0,0 +1,82 @@
using StevesBot.Worker.Handlers;
namespace StevesBot.Worker.Tests.Unit;
public class WelcomeMessageHandlerTests
{
private readonly Mock<IDiscordRestClient> _mockDiscordRestClient = new();
private readonly Mock<ILogger<IDiscordGatewayClient>> _mockLogger = new();
private readonly IServiceProvider _serviceProvider;
public WelcomeMessageHandlerTests()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(_mockDiscordRestClient.Object);
serviceCollection.AddSingleton(_mockLogger.Object);
_serviceProvider = serviceCollection.BuildServiceProvider();
}
[Fact]
public async Task HandleAsync_WhenEventIsNotMessageCreateEvent_ItShouldNotCreateMessage()
{
await WelcomeMessageHandler.HandleAsync(new DiscordEvent(), _serviceProvider);
_mockDiscordRestClient
.Verify(
static c => c.CreateMessageAsync(It.IsAny<string>(), It.IsAny<CreateMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Never
);
}
[Fact]
public async Task HandleAsync_WhenMessageCreateEventIsNotAUserJoinMessage_ItShouldNotCreateMessage()
{
var @event = new MessageCreateDiscordEvent()
{
Data = new()
{
Type = -1,
},
};
await WelcomeMessageHandler.HandleAsync(@event, _serviceProvider);
_mockDiscordRestClient
.Verify(
static c => c.CreateMessageAsync(It.IsAny<string>(), It.IsAny<CreateMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Never
);
}
[Fact]
public async Task HandleAsync_WhenUserJoinMessageCreateEventReceived_ItShouldCreateWelcomeMessage()
{
_mockDiscordRestClient
.Setup(static c => c.CreateMessageAsync(It.IsAny<string>(), It.IsAny<CreateMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new DiscordMessage());
var @event = new MessageCreateDiscordEvent()
{
Data = new()
{
Type = DiscordMessageTypes.UserJoin,
Id = Guid.NewGuid().ToString(),
ChannelId = Guid.NewGuid().ToString(),
GuildId = Guid.NewGuid().ToString(),
Author = new()
{
Id = Guid.NewGuid().ToString(),
}
}
};
await WelcomeMessageHandler.HandleAsync(@event, _serviceProvider);
_mockDiscordRestClient
.Verify(
static c => c.CreateMessageAsync(It.IsAny<string>(), It.IsAny<CreateMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Once
);
}
}
@@ -0,0 +1,28 @@
global using System.Net;
global using System.Net.WebSockets;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Hosting.Server.Features;
global using Microsoft.AspNetCore.Http.Features;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Moq;
global using RichardSzalay.MockHttp;
global using StevesBot.Worker.Discord;
global using StevesBot.Worker.Discord.Gateway;
global using StevesBot.Worker.Discord.Gateway.Events;
global using StevesBot.Worker.Discord.Gateway.Events.Data;
global using StevesBot.Worker.Discord.Rest;
global using StevesBot.Worker.Discord.Rest.Requests;
global using StevesBot.Worker.Discord.Shared;
global using StevesBot.Worker.Tests.Integration.Infrastructure;
global using StevesBot.Worker.Threading;
global using StevesBot.Worker.WebSockets;