diff --git a/.editorconfig b/.editorconfig index 2897f41f..2c250293 100644 --- a/.editorconfig +++ b/.editorconfig @@ -413,6 +413,9 @@ dotnet_diagnostic.Moq1400.severity = none # Choose a mock behaviour (strict, loo dotnet_diagnostic.Moq1410.severity = none # Set behaviour to Strict dotnet_diagnostic.S2094.severity = none # Empty classes — intentional discriminated-union marker records (Marker/Note/EventArg hierarchies) +dotnet_diagnostic.S3776.severity = warning # Cognitive complexity — fails locally on what SonarQube flags as Critical instead of only on the server + +dotnet_diagnostic.VSTHRD202.severity = warning # Return the Task directly when async/await adds nothing — SonarQube imports this INFO diagnostic as a code smell dotnet_diagnostic.T0003.severity = none # Do not use tuples dotnet_diagnostic.T0006.severity = none # Do not use Helper in type name diff --git a/src/RustPlusBot.Features.Alarms/Pairing/AlarmPairingCoordinator.cs b/src/RustPlusBot.Features.Alarms/Pairing/AlarmPairingCoordinator.cs index f40b9e41..31531843 100644 --- a/src/RustPlusBot.Features.Alarms/Pairing/AlarmPairingCoordinator.cs +++ b/src/RustPlusBot.Features.Alarms/Pairing/AlarmPairingCoordinator.cs @@ -127,10 +127,10 @@ await store.SetMessageIdAsync(guildId, serverId, entityId, mid, cancellationToke public bool TryDismiss(ulong guildId, Guid serverId, ulong entityId) => _pending.TryRemove((guildId, serverId, entityId), out _); - private static async Task GetCultureAsync(IServiceProvider provider, ulong guildId, CancellationToken ct) + private static Task GetCultureAsync(IServiceProvider provider, ulong guildId, CancellationToken ct) { var store = provider.GetRequiredService(); - return await store.GetCultureAsync(guildId, ct).ConfigureAwait(false); + return store.GetCultureAsync(guildId, ct); } private sealed record Pending(string DefaultName, ulong? MessageId); diff --git a/src/RustPlusBot.Features.Alarms/Relaying/AlarmRefresher.cs b/src/RustPlusBot.Features.Alarms/Relaying/AlarmRefresher.cs index 53badfe7..85670551 100644 --- a/src/RustPlusBot.Features.Alarms/Relaying/AlarmRefresher.cs +++ b/src/RustPlusBot.Features.Alarms/Relaying/AlarmRefresher.cs @@ -73,12 +73,12 @@ await store.SetMessageIdAsync(alarm.GuildId, alarm.ServerId, alarm.EntityId, mid } } - private static async Task GetCultureAsync( + private static Task GetCultureAsync( IServiceProvider provider, ulong guildId, CancellationToken ct) { var store = provider.GetRequiredService(); - return await store.GetCultureAsync(guildId, ct).ConfigureAwait(false); + return store.GetCultureAsync(guildId, ct); } } diff --git a/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs index 24f760c7..a0235dd4 100644 --- a/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs +++ b/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs @@ -23,12 +23,11 @@ internal sealed class CargoCommandHandler( public string Name => "cargo"; /// - public async Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) + public Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); - return await MarkerReply + return MarkerReply .ForAsync(new MarkerReplyServices(state, localizer, clock, mapSettings), context, MarkerKind.CargoShip, - "command.cargo", cancellationToken) - .ConfigureAwait(false); + "command.cargo", cancellationToken); } } diff --git a/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs index 2039de8f..519bb5fa 100644 --- a/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs +++ b/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs @@ -23,12 +23,11 @@ internal sealed class ChinookCommandHandler( public string Name => "chinook"; /// - public async Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) + public Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); - return await MarkerReply + return MarkerReply .ForAsync(new MarkerReplyServices(state, localizer, clock, mapSettings), context, MarkerKind.Chinook, - "command.chinook", cancellationToken) - .ConfigureAwait(false); + "command.chinook", cancellationToken); } } diff --git a/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs index ee5c9d75..9d5cf879 100644 --- a/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs +++ b/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs @@ -23,12 +23,11 @@ internal sealed class HeliCommandHandler( public string Name => "heli"; /// - public async Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) + public Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); - return await MarkerReply + return MarkerReply .ForAsync(new MarkerReplyServices(state, localizer, clock, mapSettings), context, - MarkerKind.PatrolHelicopter, "command.heli", cancellationToken) - .ConfigureAwait(false); + MarkerKind.PatrolHelicopter, "command.heli", cancellationToken); } } diff --git a/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs b/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs index 1ce924ac..dd3df827 100644 --- a/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs +++ b/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs @@ -15,7 +15,7 @@ internal static class MarkerReply /// The localization key prefix ("command.cargo" / "command.heli" / "command.chinook"). /// A cancellation token. /// The localized reply. - public static async Task ForAsync( + public static async Task ForAsync( MarkerReplyServices services, CommandContext context, MarkerKind kind, diff --git a/src/RustPlusBot.Features.Connections/Modules/ServerRemovalModule.cs b/src/RustPlusBot.Features.Connections/Modules/ServerRemovalModule.cs index c89131b8..056b3a4f 100644 --- a/src/RustPlusBot.Features.Connections/Modules/ServerRemovalModule.cs +++ b/src/RustPlusBot.Features.Connections/Modules/ServerRemovalModule.cs @@ -74,6 +74,5 @@ await FollowupAsync(removed ? "Server removed." : "That server no longer exists. /// Cancels the removal. [ComponentInteraction(RemoveCancelId)] - public async Task RemoveCancelAsync() => - await RespondAsync("Cancelled.", ephemeral: true).ConfigureAwait(false); + public Task RemoveCancelAsync() => RespondAsync("Cancelled.", ephemeral: true); } diff --git a/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs b/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs index 42c6a15e..fba477e7 100644 --- a/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs +++ b/src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs @@ -216,50 +216,48 @@ public async Task StopAllAsync() } /// - public async Task GetServerInfoAsync( + public Task GetServerInfoAsync( ulong guildId, Guid serverId, CancellationToken cancellationToken) { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return null; + return Task.FromResult(null); } - return await live.Connection.GetServerInfoAsync(_options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection.GetServerInfoAsync(_options.HeartbeatTimeout, cancellationToken); } /// - public async Task GetTimeAsync(ulong guildId, + public Task GetTimeAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken) { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return null; + return Task.FromResult(null); } - return await live.Connection.GetTimeAsync(_options.HeartbeatTimeout, cancellationToken).ConfigureAwait(false); + return live.Connection.GetTimeAsync(_options.HeartbeatTimeout, cancellationToken); } /// - public async Task GetTeamInfoAsync( + public Task GetTeamInfoAsync( ulong guildId, Guid serverId, CancellationToken cancellationToken) { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return null; + return Task.FromResult(null); } - return await live.Connection.GetTeamInfoAsync(_options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection.GetTeamInfoAsync(_options.HeartbeatTimeout, cancellationToken); } /// - public async Task PromoteToLeaderAsync( + public Task PromoteToLeaderAsync( ulong guildId, Guid serverId, ulong steamId, @@ -267,11 +265,10 @@ public async Task PromoteToLeaderAsync( { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return false; + return Task.FromResult(false); } - return await live.Connection.PromoteToLeaderAsync(steamId, _options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection.PromoteToLeaderAsync(steamId, _options.HeartbeatTimeout, cancellationToken); } /// @@ -331,15 +328,14 @@ public async Task PromoteToLeaderAsync( } /// - public async Task GetWorldAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken) + public Task GetWorldAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken) { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return null; + return Task.FromResult(null); } - return await live.Connection.GetWorldAsync(_options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection.GetWorldAsync(_options.HeartbeatTimeout, cancellationToken); } /// @@ -392,7 +388,7 @@ public async Task> GetMonumentsAsync( } /// - public async Task GetSmartAlarmReadingAsync( + public Task GetSmartAlarmReadingAsync( ulong guildId, Guid serverId, ulong entityId, @@ -400,12 +396,11 @@ public async Task GetSmartAlarmReadingAsync( { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return new DeviceReading(null, DeviceReachability.NoResponse); + return Task.FromResult(new DeviceReading(null, DeviceReachability.NoResponse)); } - return await live.Connection - .GetSmartDeviceInfoAsync(entityId, SmartDeviceKind.Alarm, _options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection + .GetSmartDeviceInfoAsync(entityId, SmartDeviceKind.Alarm, _options.HeartbeatTimeout, cancellationToken); } /// @@ -427,7 +422,7 @@ public async Task GetSmartAlarmReadingAsync( } /// - public async Task SetSmartSwitchAsync( + public Task SetSmartSwitchAsync( ulong guildId, Guid serverId, ulong entityId, @@ -436,16 +431,14 @@ public async Task SetSmartSwitchAsync( { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return DeviceReachability.NoResponse; + return Task.FromResult(DeviceReachability.NoResponse); } - return await live.Connection - .SetSmartSwitchValueAsync(entityId, value, _options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection.SetSmartSwitchValueAsync(entityId, value, _options.HeartbeatTimeout, cancellationToken); } /// - public async Task StrobeSmartSwitchAsync( + public Task StrobeSmartSwitchAsync( ulong guildId, Guid serverId, ulong entityId, @@ -455,16 +448,15 @@ public async Task StrobeSmartSwitchAsync( { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return DeviceReachability.NoResponse; + return Task.FromResult(DeviceReachability.NoResponse); } - return await live.Connection - .StrobeSmartSwitchAsync(entityId, timeoutMs, value, _options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection + .StrobeSmartSwitchAsync(entityId, timeoutMs, value, _options.HeartbeatTimeout, cancellationToken); } /// - public async Task SetClanMotdAsync( + public Task SetClanMotdAsync( ulong guildId, Guid serverId, string motd, @@ -472,12 +464,10 @@ public async Task SetClanMotdAsync( { if (!_liveSockets.TryGetValue((guildId, serverId), out var live)) { - return false; + return Task.FromResult(false); } - return await live.Connection - .SetClanMotdAsync(motd, _options.HeartbeatTimeout, cancellationToken) - .ConfigureAwait(false); + return live.Connection.SetClanMotdAsync(motd, _options.HeartbeatTimeout, cancellationToken); } [LoggerMessage(Level = LogLevel.Error, Message = "Connection loop for server {ServerId} faulted.")] @@ -521,37 +511,11 @@ await PublishStatusAsync(key, ConnectionStatus.Connecting, null, p.CredentialId, .ConfigureAwait(false); var connection = source.Create(p.Ip, p.Port, p.SteamId, p.PlayerToken); - SocketConnectOutcome outcome; - try - { - outcome = await connection.ConnectAsync(_options.ConnectTimeout, ct).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - await connection.DisposeAsync().ConfigureAwait(false); - throw; - } - catch (Exception) when (ct.IsCancellationRequested) + var outcome = await ConnectAsync(connection, key.Server, ct).ConfigureAwait(false); + if (outcome is null) { - // Stopping. A socket library may unwind cancellation as something other than an - // OperationCanceledException (a client disposed underneath the connect, say). Dispose - // and end the loop as the cancellation it is — reporting the server unreachable and - // warning about a connect failure here would be noise pointing at the wrong problem. - await connection.DisposeAsync().ConfigureAwait(false); return; } -#pragma warning disable CA1031 // Broad catch is intentional: see below — a throwing source must not end the loop. - catch (Exception ex) -#pragma warning restore CA1031 - { - // A source that throws instead of reporting an outcome is still just an unreachable - // server. Letting it escape reaches the outer catch and ENDS the loop, and nothing - // re-arms a dead loop, so the server would stay offline until the process restarts. - // Fall through as Unreachable: the branch below disposes the socket, publishes the - // status and backs off, exactly as for a reported failure. - LogConnectThrew(logger, ex, key.Server); - outcome = SocketConnectOutcome.Unreachable; - } if (outcome == SocketConnectOutcome.AuthRejected) { @@ -612,6 +576,53 @@ await PublishStatusAsync(key, ConnectionStatus.Unreachable, null, p.CredentialId #pragma warning restore CA1031 } + /// + /// Connects the socket, folding a throwing source into an outcome so the connection loop never ends on it. + /// + /// The freshly created socket to connect. + /// The server the socket belongs to, for logging. + /// The loop's cancellation token. + /// + /// The connect outcome, or when the connect unwound because the loop is stopping; + /// the socket is disposed in that case and the caller just ends the loop. + /// + private async Task ConnectAsync( + IRustServerConnection connection, + Guid serverId, + CancellationToken ct) + { + try + { + return await connection.ConnectAsync(_options.ConnectTimeout, ct).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await connection.DisposeAsync().ConfigureAwait(false); + throw; + } + catch (Exception) when (ct.IsCancellationRequested) + { + // Stopping. A socket library may unwind cancellation as something other than an + // OperationCanceledException (a client disposed underneath the connect, say). Dispose + // and end the loop as the cancellation it is — reporting the server unreachable and + // warning about a connect failure here would be noise pointing at the wrong problem. + await connection.DisposeAsync().ConfigureAwait(false); + return null; + } +#pragma warning disable CA1031 // Broad catch is intentional: see below — a throwing source must not end the loop. + catch (Exception ex) +#pragma warning restore CA1031 + { + // A source that throws instead of reporting an outcome is still just an unreachable + // server. Letting it escape reaches the loop's outer catch and ENDS the loop, and nothing + // re-arms a dead loop, so the server would stay offline until the process restarts. + // Report Unreachable instead: the loop disposes the socket, publishes the status and + // backs off, exactly as for a reported failure. + LogConnectThrew(logger, ex, serverId); + return SocketConnectOutcome.Unreachable; + } + } + private async Task RunConnectedAsync( (ulong Guild, Guid Server) key, IRustServerConnection connection, diff --git a/src/RustPlusBot.Features.Pairing/Hosting/PairingHostedService.cs b/src/RustPlusBot.Features.Pairing/Hosting/PairingHostedService.cs index 5a114da6..b3347616 100644 --- a/src/RustPlusBot.Features.Pairing/Hosting/PairingHostedService.cs +++ b/src/RustPlusBot.Features.Pairing/Hosting/PairingHostedService.cs @@ -24,10 +24,10 @@ public Task StartAsync(CancellationToken cancellationToken) } /// - public async Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(CancellationToken cancellationToken) { client.Ready -= OnReadyAsync; - await supervisor.StopAllAsync().ConfigureAwait(false); + return supervisor.StopAllAsync(); } private Task OnReadyAsync() diff --git a/src/RustPlusBot.Features.Pairing/Modules/CredentialModule.cs b/src/RustPlusBot.Features.Pairing/Modules/CredentialModule.cs index 7deea32f..7ba5d253 100644 --- a/src/RustPlusBot.Features.Pairing/Modules/CredentialModule.cs +++ b/src/RustPlusBot.Features.Pairing/Modules/CredentialModule.cs @@ -136,6 +136,5 @@ await FollowupAsync($"Account disconnected. Removed from {count} server(s).", ep /// Cancels the disconnect. [ComponentInteraction(DisconnectCancelId)] - public async Task DisconnectCancelAsync() => - await RespondAsync("Cancelled.", ephemeral: true).ConfigureAwait(false); + public Task DisconnectCancelAsync() => RespondAsync("Cancelled.", ephemeral: true); } diff --git a/src/RustPlusBot.Features.StorageMonitors/Relaying/StorageMonitorStateRelay.cs b/src/RustPlusBot.Features.StorageMonitors/Relaying/StorageMonitorStateRelay.cs index ffd1cb4e..b66cf68f 100644 --- a/src/RustPlusBot.Features.StorageMonitors/Relaying/StorageMonitorStateRelay.cs +++ b/src/RustPlusBot.Features.StorageMonitors/Relaying/StorageMonitorStateRelay.cs @@ -159,12 +159,12 @@ await store.SetMessageIdAsync(guildId, serverId, monitor.EntityId, mid, cancella } } - private static async Task GetCultureAsync( + private static Task GetCultureAsync( IServiceProvider provider, ulong guildId, CancellationToken cancellationToken) { var store = provider.GetRequiredService(); - return await store.GetCultureAsync(guildId, cancellationToken).ConfigureAwait(false); + return store.GetCultureAsync(guildId, cancellationToken); } } diff --git a/src/RustPlusBot.Features.Switches/Relaying/SwitchStateRelay.cs b/src/RustPlusBot.Features.Switches/Relaying/SwitchStateRelay.cs index 9c75c30e..3370c0e1 100644 --- a/src/RustPlusBot.Features.Switches/Relaying/SwitchStateRelay.cs +++ b/src/RustPlusBot.Features.Switches/Relaying/SwitchStateRelay.cs @@ -208,12 +208,12 @@ await store.SetMessageIdAsync(guildId, serverId, sw.EntityId, mid, cancellationT } } - private static async Task GetCultureAsync( + private static Task GetCultureAsync( IServiceProvider provider, ulong guildId, CancellationToken cancellationToken) { var store = provider.GetRequiredService(); - return await store.GetCultureAsync(guildId, cancellationToken).ConfigureAwait(false); + return store.GetCultureAsync(guildId, cancellationToken); } } diff --git a/src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs b/src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs index 3c297703..454567b0 100644 --- a/src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs +++ b/src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs @@ -181,7 +181,7 @@ await channel.DeleteMessageAsync(messageId, Options(cancellationToken)) } /// - public async Task EnsureChannelOrderAsync(ulong guildId, + public Task EnsureChannelOrderAsync(ulong guildId, ulong categoryId, IReadOnlyList orderedChannelIds, CancellationToken cancellationToken) @@ -190,7 +190,7 @@ public async Task EnsureChannelOrderAsync(ulong guildId, var guild = client.GetGuild(guildId); if (guild is null) { - return; + return Task.CompletedTask; } var live = orderedChannelIds @@ -199,25 +199,25 @@ public async Task EnsureChannelOrderAsync(ulong guildId, .ToList(); if (live.Count < 2) { - return; + return Task.CompletedTask; } // Discord sorts a category's channels by position, ties by snowflake. var current = live.OrderBy(c => c.Position).ThenBy(c => c.Id).Select(c => c.Id); if (current.SequenceEqual(live.Select(c => c.Id))) { - return; + return Task.CompletedTask; } // Permute the channels' existing position values rather than assigning fresh ones, so every // channel outside the list keeps its place relative to the reordered block. var slots = live.Select(c => c.Position).Order().ToList(); - await guild.ReorderChannelsAsync( + return guild.ReorderChannelsAsync( live.Select((c, i) => new ReorderChannelProperties(c.Id, slots[i])), new RequestOptions { CancelToken = cancellationToken - }).ConfigureAwait(false); + }); } /// diff --git a/tests/RustPlusBot.Features.Chat.Tests/Hosting/ChatHostedServiceTests.cs b/tests/RustPlusBot.Features.Chat.Tests/Hosting/ChatHostedServiceTests.cs index 70dca6bf..db918044 100644 --- a/tests/RustPlusBot.Features.Chat.Tests/Hosting/ChatHostedServiceTests.cs +++ b/tests/RustPlusBot.Features.Chat.Tests/Hosting/ChatHostedServiceTests.cs @@ -234,11 +234,11 @@ await bus.PublishAsync(new ClanMessageReceivedEvent(10UL, Guid.NewGuid(), 7UL, " #pragma warning disable S2699 // The implicit assertion is "no exception is thrown". [Fact] - public async Task StopAsync_without_a_start_completes() + public Task StopAsync_without_a_start_completes() { var (service, _, _, _) = Build(); - await service.StopAsync(default); + return service.StopAsync(default); } #pragma warning restore S2699 diff --git a/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs b/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs index abbf4c1f..8af9e34e 100644 --- a/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs +++ b/tests/RustPlusBot.Features.Map.Tests/MapComposerTests.cs @@ -156,7 +156,7 @@ public async Task Tunnel_tokens_render_under_the_tunnels_layer_not_the_monuments Assert.False(tunResult.Png.SequenceEqual(baseResult.Png), "tunnel token must render under the Tunnels layer"); } - private static async Task ComposeTunnelScenarioAsync(bool monuments, bool tunnels) + private static Task ComposeTunnelScenarioAsync(bool monuments, bool tunnels) { var query = NewQuery(); query.GetMonumentsAsync(Guild, Server, Arg.Any()) @@ -165,7 +165,7 @@ public async Task Tunnel_tokens_render_under_the_tunnels_layer_not_the_monuments Grid: false, Markers: false, Monuments: monuments, Vendor: false, Players: false, Rigs: false, Tunnels: tunnels); var composer = Build(BaseJpeg(), Dims, query, NewEvents(), NewRigs(), NewSettings(settings)); - return await composer.ComposeAsync(Guild, Server, CancellationToken.None); + return composer.ComposeAsync(Guild, Server, CancellationToken.None); } [Fact]