From 9e74dac5edb7bfdbc86420b6bb285b20bcd02ada Mon Sep 17 00:00:00 2001 From: RaidMax Date: Tue, 21 Apr 2020 17:34:00 -0500 Subject: [PATCH] fix stat issue with concurrent threads fix potential lost penalty if server does not response to kick request make sure that broadcast only shows one custom say name add unit tests --- Application/IW4MServer.cs | 191 ++++----- .../DynamicRConParserConfiguration.cs | 2 +- Plugins/Stats/Models/EFClientStatistics.cs | 5 +- SharedLibraryCore/PartialEntities/EFClient.cs | 3 + SharedLibraryCore/Server.cs | 13 +- SharedLibraryCore/Services/PenaltyService.cs | 4 +- SharedLibraryCore/Utilities.cs | 25 ++ .../Fixtures/ClientGenerators.cs | 24 ++ .../Fixtures/ConfigurationGenerators.cs | 22 + Tests/ApplicationTests/IW4MServerTests.cs | 375 ++++++++++++++++++ Tests/ApplicationTests/Mocks/EventHandler.cs | 16 + 11 files changed, 554 insertions(+), 126 deletions(-) create mode 100644 Tests/ApplicationTests/Fixtures/ClientGenerators.cs create mode 100644 Tests/ApplicationTests/Fixtures/ConfigurationGenerators.cs create mode 100644 Tests/ApplicationTests/Mocks/EventHandler.cs diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 498679fba..3442d792f 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -1096,158 +1096,111 @@ namespace IW4MAdmin return logPath.FixDirectoryCharacters(); } - protected override async Task Warn(String Reason, EFClient Target, EFClient Origin) + public override async Task Warn(string reason, EFClient targetClient, EFClient targetOrigin) { // ensure player gets warned if command not performed on them in game - if (Target.ClientNumber < 0) - { - var ingameClient = Manager.GetActiveClients() - .FirstOrDefault(c => c.ClientId == Target.ClientId); - - if (ingameClient != null) - { - await Warn(Reason, ingameClient, Origin); - return; - } - } - - else - { - if (Target.Warnings >= 4) - { - Target.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this)); - return; - } - - string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}"; - Target.CurrentServer.Broadcast(message); - } + targetClient = targetClient.ClientNumber < 0 ? + Manager.GetActiveClients() + .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient : + targetClient; var newPenalty = new EFPenalty() { Type = EFPenalty.PenaltyType.Warning, Expires = DateTime.UtcNow, - Offender = Target, - Punisher = Origin, - Offense = Reason, - Link = Target.AliasLink + Offender = targetClient, + Punisher = targetOrigin, + Offense = reason, + Link = targetClient.AliasLink }; - await Manager.GetPenaltyService().Create(newPenalty); - } + Logger.WriteDebug($"Creating warn penalty for {targetClient}"); + await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); - protected override async Task Kick(String Reason, EFClient Target, EFClient Origin) - { - // ensure player gets kicked if command not performed on them in game - if (Target.ClientNumber < 0) + if (targetClient.IsIngame) { - var ingameClient = Manager.GetActiveClients() - .FirstOrDefault(c => c.ClientId == Target.ClientId); - - if (ingameClient != null) + if (targetClient.Warnings >= 4) { - await Kick(Reason, ingameClient, Origin); + targetClient.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this)); return; } - } -#if !DEBUG - else - { - string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); - await Target.CurrentServer.ExecuteCommandAsync(formattedKick); - } -#endif -#if DEBUG - // await Target.CurrentServer.OnClientDisconnected(Target); - var e = new GameEvent() - { - Type = GameEvent.EventType.PreDisconnect, - Origin = Target, - Owner = this - }; + string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}"; + targetClient.CurrentServer.Broadcast(message); + } + } - Manager.GetEventHandler().AddEvent(e); -#endif + public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient) + { + targetClient = targetClient.ClientNumber < 0 ? + Manager.GetActiveClients() + .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient : + targetClient; var newPenalty = new EFPenalty() { Type = EFPenalty.PenaltyType.Kick, Expires = DateTime.UtcNow, - Offender = Target, + Offender = targetClient, Offense = Reason, - Punisher = Origin, - Link = Target.AliasLink + Punisher = originClient, + Link = targetClient.AliasLink }; - await Manager.GetPenaltyService().Create(newPenalty); + Logger.WriteDebug($"Creating kick penalty for {targetClient}"); + await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); + + if (targetClient.IsIngame) + { + var e = new GameEvent() + { + Type = GameEvent.EventType.PreDisconnect, + Origin = targetClient, + Owner = this + }; + + Manager.GetEventHandler().AddEvent(e); + + string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); + await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); + } } - protected override async Task TempBan(String Reason, TimeSpan length, EFClient Target, EFClient Origin) + public override async Task TempBan(string Reason, TimeSpan length, EFClient targetClient, EFClient originClient) { - // ensure player gets banned if command not performed on them in game - if (Target.ClientNumber < 0) - { - var ingameClient = Manager.GetActiveClients() - .FirstOrDefault(c => c.ClientId == Target.ClientId); - - if (ingameClient != null) - { - await TempBan(Reason, length, ingameClient, Origin); - return; - } - } -#if !DEBUG - else - { - string formattedKick = String.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); - await Target.CurrentServer.ExecuteCommandAsync(formattedKick); - } -#else - await Target.CurrentServer.OnClientDisconnected(Target); -#endif + // ensure player gets kicked if command not performed on them in the same server + targetClient = targetClient.ClientNumber < 0 ? + Manager.GetActiveClients() + .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient : + targetClient; var newPenalty = new EFPenalty() { Type = EFPenalty.PenaltyType.TempBan, Expires = DateTime.UtcNow + length, - Offender = Target, + Offender = targetClient, Offense = Reason, - Punisher = Origin, - Link = Target.AliasLink + Punisher = originClient, + Link = targetClient.AliasLink }; - await Manager.GetPenaltyService().Create(newPenalty); + Logger.WriteDebug($"Creating tempban penalty for {targetClient}"); + await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); + + if (targetClient.IsIngame) + { + string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); + await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); + } } - override protected async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false) + override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false) { - // ensure player gets banned if command not performed on them in game - if (targetClient.ClientNumber < 0) - { - EFClient ingameClient = null; - - ingameClient = Manager.GetServers() - .Select(s => s.GetClientsAsList()) - .FirstOrDefault(l => l.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) != null) - ?.First(c => c.ClientId == targetClient.ClientId); - - if (ingameClient != null) - { - await Ban(reason, ingameClient, originClient, isEvade); - return; - } - } - - else - { -#if !DEBUG - string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7"); - await targetClient.CurrentServer.ExecuteCommandAsync(formattedString); -#else - await targetClient.CurrentServer.OnClientDisconnected(targetClient); -#endif - } + // ensure player gets kicked if command not performed on them in the same server + targetClient = targetClient.ClientNumber < 0 ? + Manager.GetActiveClients() + .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient : + targetClient; EFPenalty newPenalty = new EFPenalty() { @@ -1260,8 +1213,16 @@ namespace IW4MAdmin IsEvadedOffense = isEvade }; + Logger.WriteDebug($"Creating ban penalty for {targetClient}"); targetClient.SetLevel(Permission.Banned, originClient); - await Manager.GetPenaltyService().Create(newPenalty); + await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); + + if (targetClient.IsIngame) + { + Logger.WriteDebug($"Attempting to kicking newly banned client {targetClient}"); + string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7"); + await targetClient.CurrentServer.ExecuteCommandAsync(formattedString); + } } override public async Task Unban(string reason, EFClient Target, EFClient Origin) diff --git a/Application/RconParsers/DynamicRConParserConfiguration.cs b/Application/RconParsers/DynamicRConParserConfiguration.cs index 12309f458..79df12504 100644 --- a/Application/RconParsers/DynamicRConParserConfiguration.cs +++ b/Application/RconParsers/DynamicRConParserConfiguration.cs @@ -9,7 +9,7 @@ namespace IW4MAdmin.Application.RconParsers /// generic implementation of the IRConParserConfiguration /// allows script plugins to generate dynamic RCon configurations /// - sealed internal class DynamicRConParserConfiguration : IRConParserConfiguration + public class DynamicRConParserConfiguration : IRConParserConfiguration { public CommandPrefix CommandPrefixes { get; set; } public ParserRegex Status { get; set; } diff --git a/Plugins/Stats/Models/EFClientStatistics.cs b/Plugins/Stats/Models/EFClientStatistics.cs index c406956c7..635aa1710 100644 --- a/Plugins/Stats/Models/EFClientStatistics.cs +++ b/Plugins/Stats/Models/EFClientStatistics.cs @@ -82,7 +82,10 @@ namespace IW4MAdmin.Plugins.Stats.Models KillStreak = 0; DeathStreak = 0; LastScore = 0; - SessionScores.Add(0); + lock (SessionScores) + { + SessionScores.Add(0); + } Team = IW4Info.Team.None; } [NotMapped] diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index b25a94eaa..7dca84d57 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -124,6 +124,9 @@ namespace SharedLibraryCore.Database.Models [NotMapped] public string IPAddressString => IPAddress.ConvertIPtoString(); + [NotMapped] + public bool IsIngame => ClientNumber >= 0; + [NotMapped] public virtual IDictionary LinkedAccounts { get; set; } diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 2ca24a086..ee176b7ca 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -121,8 +121,7 @@ namespace SharedLibraryCore /// Message to be sent to all players public GameEvent Broadcast(string message, EFClient sender = null) { - string formattedMessage = String.Format(RconParser.Configuration.CommandPrefixes.Say, $"{(CustomSayEnabled ? $"{CustomSayName}: " : "")}{message.FixIW4ForwardSlash()}"); - + string formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say, $"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message.FixIW4ForwardSlash()}"); #if DEBUG == true Logger.WriteVerbose(message.StripColors()); #endif @@ -196,14 +195,14 @@ namespace SharedLibraryCore /// /// Reason for kicking /// EFClient to kick - abstract protected Task Kick(String Reason, EFClient Target, EFClient Origin); + abstract public Task Kick(String Reason, EFClient Target, EFClient Origin); /// /// Temporarily ban a player ( default 1 hour ) from the server /// /// Reason for banning the player /// The player to ban - abstract protected Task TempBan(String Reason, TimeSpan length, EFClient Target, EFClient Origin); + abstract public Task TempBan(String Reason, TimeSpan length, EFClient Target, EFClient Origin); /// /// Perm ban a player from the server @@ -211,9 +210,9 @@ namespace SharedLibraryCore /// The reason for the ban /// The person to ban /// The person who banned the target - abstract protected Task Ban(String Reason, EFClient Target, EFClient Origin, bool isEvade = false); + abstract public Task Ban(String Reason, EFClient Target, EFClient Origin, bool isEvade = false); - abstract protected Task Warn(String Reason, EFClient Target, EFClient Origin); + abstract public Task Warn(String Reason, EFClient Target, EFClient Origin); /// /// Unban a player by npID / GUID @@ -307,7 +306,7 @@ namespace SharedLibraryCore public bool CustomCallback { get; protected set; } public string WorkingDirectory { get; protected set; } public IRConConnection RemoteConnection { get; protected set; } - public IRConParser RconParser { get; protected set; } + public IRConParser RconParser { get; set; } public IEventParser EventParser { get; set; } public string LogPath { get; protected set; } public bool RestartRequested { get; set; } diff --git a/SharedLibraryCore/Services/PenaltyService.cs b/SharedLibraryCore/Services/PenaltyService.cs index 61105180a..0939d55cd 100644 --- a/SharedLibraryCore/Services/PenaltyService.cs +++ b/SharedLibraryCore/Services/PenaltyService.cs @@ -12,7 +12,7 @@ namespace SharedLibraryCore.Services { public class PenaltyService : Interfaces.IEntityService { - public async Task Create(EFPenalty newEntity) + public virtual async Task Create(EFPenalty newEntity) { using (var context = new DatabaseContext()) { @@ -181,7 +181,7 @@ namespace SharedLibraryCore.Services } } - public async Task RemoveActivePenalties(int aliasLinkId) + public virtual async Task RemoveActivePenalties(int aliasLinkId) { using (var context = new DatabaseContext()) { diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 69e7bd96b..ad6a342b4 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Storage; #endif using SharedLibraryCore.Database.Models; using SharedLibraryCore.Helpers; +using SharedLibraryCore.Interfaces; using System; using System.Collections.Generic; using System.Diagnostics; @@ -875,6 +876,30 @@ namespace SharedLibraryCore return new[] { deltaX, deltaY }; } + /// + /// attempts to create and persist a penalty + /// + /// + /// + /// + /// true of the creat succeeds, false otherwise + public static async Task TryCreatePenalty(this EFPenalty penalty, IEntityService penaltyService, ILogger logger) + { + try + { + await penaltyService.Create(penalty); + return true; + } + + catch (Exception e) + { + logger.WriteWarning($"Could not create penalty of type {penalty.Type.ToString()}"); + logger.WriteDebug(e.GetExceptionInfo()); + } + + return false; + } + public static bool ShouldHideLevel(this Permission perm) => perm == Permission.Flagged; /// diff --git a/Tests/ApplicationTests/Fixtures/ClientGenerators.cs b/Tests/ApplicationTests/Fixtures/ClientGenerators.cs new file mode 100644 index 000000000..29c0c41fe --- /dev/null +++ b/Tests/ApplicationTests/Fixtures/ClientGenerators.cs @@ -0,0 +1,24 @@ +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ApplicationTests.Fixtures +{ + public class ClientGenerators + { + public static EFClient CreateBasicClient(Server currentServer, bool isIngame = true) => new EFClient() + { + ClientId = 1, + CurrentAlias = new EFAlias() + { + Name = "BasicClient", + IPAddress = "127.0.0.1".ConvertToIP(), + }, + Level = EFClient.Permission.User, + ClientNumber = isIngame ? 0 : -1, + CurrentServer = currentServer + }; + } +} diff --git a/Tests/ApplicationTests/Fixtures/ConfigurationGenerators.cs b/Tests/ApplicationTests/Fixtures/ConfigurationGenerators.cs new file mode 100644 index 000000000..7e3b70a70 --- /dev/null +++ b/Tests/ApplicationTests/Fixtures/ConfigurationGenerators.cs @@ -0,0 +1,22 @@ +using IW4MAdmin.Application.RconParsers; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ApplicationTests.Fixtures +{ + public class ConfigurationGenerators + { + public static ServerConfiguration CreateServerConfiguration() => new ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }; + public static IRConParserConfiguration CreateRConParserConfiguration(IParserRegexFactory factory) => new DynamicRConParserConfiguration(factory) + { + CommandPrefixes = new SharedLibraryCore.RCon.CommandPrefix() + { + Kick = "kick", + Say = "say" + } + }; + } +} diff --git a/Tests/ApplicationTests/IW4MServerTests.cs b/Tests/ApplicationTests/IW4MServerTests.cs index e6607a31f..9688a1b4b 100644 --- a/Tests/ApplicationTests/IW4MServerTests.cs +++ b/Tests/ApplicationTests/IW4MServerTests.cs @@ -1,12 +1,55 @@ using IW4MAdmin; using IW4MAdmin.Application.Misc; +using SharedLibraryCore.Interfaces; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using FakeItEasy; +using System; +using ApplicationTests.Fixtures; +using SharedLibraryCore.Services; +using SharedLibraryCore.Database.Models; +using System.Threading.Tasks; +using ApplicationTests.Mocks; +using System.Linq; namespace ApplicationTests { [TestFixture] public class IW4MServerTests { + private IServiceProvider serviceProvider; + private ILogger fakeLogger; + private IManager fakeManager; + private IRConConnection fakeRConConnection; + private IRConParser fakeRConParser; + private MockEventHandler mockEventHandler; + + [SetUp] + public void Setup() + { + fakeLogger = A.Fake(); + fakeManager = A.Fake(); + fakeRConConnection = A.Fake(); + var rconConnectionFactory = A.Fake(); + A.CallTo(() => rconConnectionFactory.CreateConnection(A.Ignored, A.Ignored, A.Ignored)) + .Returns(fakeRConConnection); + var fakeTranslationLookup = A.Fake(); + fakeRConParser = A.Fake(); + A.CallTo(() => fakeRConParser.Configuration) + .Returns(ConfigurationGenerators.CreateRConParserConfiguration(A.Fake())); + + mockEventHandler = new MockEventHandler(); + A.CallTo(() => fakeManager.GetEventHandler()) + .Returns(mockEventHandler); + + serviceProvider = new ServiceCollection() + .AddSingleton(new IW4MServer(fakeManager, ConfigurationGenerators.CreateServerConfiguration(), fakeTranslationLookup, rconConnectionFactory) + { + RconParser = fakeRConParser + }) + .BuildServiceProvider(); + } + [Test] public void Test_GenerateLogPath_Basic() { @@ -133,5 +176,337 @@ namespace ApplicationTests Assert.AreEqual(expected, generated); } + + #region BAN + [Test] + public async Task Test_BanCreatesPenalty() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Ban("test reason", target, origin); + + A.CallTo(() => fakePenaltyService.Create(A.Ignored)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_BanExecutesKickCommand() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Ban("test reason", target, origin); + + A.CallTo(() => fakeRConParser.ExecuteCommandAsync(fakeRConConnection, "kick")) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_BanQueuesSetLevelEvent() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Ban("test reason", target, origin); + + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Type == SharedLibraryCore.GameEvent.EventType.ChangePermission && + _event.Origin == origin && + _event.Target == target && + (EFClient.Permission)_event.Extra == EFClient.Permission.Banned)); + } + + [Test] + public async Task Test_BanFindsIngameClientToExecuteFor() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + var target = ClientGenerators.CreateBasicClient(server, isIngame: false); + var ingameTarget = ClientGenerators.CreateBasicClient(server); + + A.CallTo(() => fakeManager.GetActiveClients()) + .Returns(new[] { ingameTarget }); + + await server.Ban("test reason", target, origin); + + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Target == ingameTarget)); + } + #endregion + + #region TEMPBAN + [Test] + public async Task Test_TempBanCreatesPenalty() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.TempBan("test reason", TimeSpan.Zero, target, origin); + + A.CallTo(() => fakePenaltyService.Create(A.Ignored)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_TempBanExecutesKickCommand() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.TempBan("test reason", TimeSpan.Zero, target, origin); + + A.CallTo(() => fakeRConParser.ExecuteCommandAsync(fakeRConConnection, "kick")) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_TempBanFindsIngameClientToExecuteFor() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + var target = ClientGenerators.CreateBasicClient(server, isIngame: false); + + var ingameTarget = ClientGenerators.CreateBasicClient(server); + + A.CallTo(() => fakeManager.GetActiveClients()) + .Returns(new[] { ingameTarget }); + + await server.TempBan("test reason", TimeSpan.Zero, target, origin); + + A.CallTo(() => fakeRConParser.ExecuteCommandAsync(fakeRConConnection, "kick")) + .MustHaveHappenedOnceExactly(); + } + #endregion + + #region KICK + [Test] + public async Task Test_KickCreatesPenalty() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Kick("test reason", target, origin); + + A.CallTo(() => fakePenaltyService.Create(A.Ignored)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_KickExecutesKickCommand() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Kick("test reason", target, origin); + + A.CallTo(() => fakeRConParser.ExecuteCommandAsync(fakeRConConnection, "kick")) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_KickQueuesPredisconnectEvent() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Kick("test reason", target, origin); + + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Type == SharedLibraryCore.GameEvent.EventType.PreDisconnect && _event.Origin == target)); + } + + [Test] + public async Task Test_KickFindsIngameClientToExecuteFor() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + var target = ClientGenerators.CreateBasicClient(server, isIngame: false); + + var ingameTarget = ClientGenerators.CreateBasicClient(server); + + A.CallTo(() => fakeManager.GetActiveClients()) + .Returns(new[] { ingameTarget }); + + await server.Kick("test reason", target, origin); + + // kick creates a pre disconnect event + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Origin == ingameTarget)); + } + #endregion + + #region WARN + [Test] + public async Task Test_WarnCreatesPenalty() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Warn("test reason", target, origin); + + A.CallTo(() => fakePenaltyService.Create(A.Ignored)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Test_WarnBroadCastMessageForIngameClient() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + + await server.Warn("test reason", target, origin); + + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Type == SharedLibraryCore.GameEvent.EventType.Broadcast)); + } + + [Test] + public async Task Test_WarnLimitReachedQueuesKickEvent() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + + var server = serviceProvider.GetRequiredService(); + var target = ClientGenerators.CreateBasicClient(server); + var origin = ClientGenerators.CreateBasicClient(server); + target.Warnings = 5; + + await server.Warn("test reason", target, origin); + + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Type == SharedLibraryCore.GameEvent.EventType.Kick && _event.Target == target)); + } + #endregion + + #region UNBAN + [Test] + public async Task Test_UnbanQueuesSetLevelEvent() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + A.CallTo(() => fakePenaltyService.RemoveActivePenalties(A.Ignored)) + .Returns(Task.CompletedTask); + + var server = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + var target = ClientGenerators.CreateBasicClient(server); + + target.Level = EFClient.Permission.Banned; + target.AliasLink = new EFAliasLink(); + + await server.Unban("test reason", target, origin); + + Assert.IsTrue(mockEventHandler.Events.Any(_event => _event.Type == SharedLibraryCore.GameEvent.EventType.ChangePermission && _event.Target == target)); + } + + [Test] + public async Task Test_UnbanRemovedActivePenalties() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + A.CallTo(() => fakePenaltyService.RemoveActivePenalties(A.Ignored)) + .Returns(Task.CompletedTask); + + var server = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + var target = ClientGenerators.CreateBasicClient(server); + + target.Level = EFClient.Permission.Banned; + target.AliasLink = new EFAliasLink() + { + AliasLinkId = 1 + }; + + await server.Unban("test reason", target, origin); + + A.CallTo(() => fakePenaltyService.RemoveActivePenalties(target.AliasLink.AliasLinkId)) + .MustHaveHappened(); + } + + [Test] + public async Task Test_UnbanCreatesPenalty() + { + var fakePenaltyService = A.Fake(); + A.CallTo(() => fakeManager.GetPenaltyService()) + .Returns(fakePenaltyService); + A.CallTo(() => fakePenaltyService.RemoveActivePenalties(A.Ignored)) + .Returns(Task.CompletedTask); + + var server = serviceProvider.GetRequiredService(); + var origin = ClientGenerators.CreateBasicClient(server); + var target = ClientGenerators.CreateBasicClient(server); + + target.Level = EFClient.Permission.Banned; + target.AliasLink = new EFAliasLink() + { + AliasLinkId = 1 + }; + + await server.Unban("test reason", target, origin); + + A.CallTo(() => fakePenaltyService.Create(A.Ignored)) + .MustHaveHappened(); + } + #endregion } } diff --git a/Tests/ApplicationTests/Mocks/EventHandler.cs b/Tests/ApplicationTests/Mocks/EventHandler.cs new file mode 100644 index 000000000..fda78c697 --- /dev/null +++ b/Tests/ApplicationTests/Mocks/EventHandler.cs @@ -0,0 +1,16 @@ +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using System.Collections.Generic; + +namespace ApplicationTests.Mocks +{ + class MockEventHandler : IEventHandler + { + public IList Events = new List(); + + public void AddEvent(GameEvent gameEvent) + { + Events.Add(gameEvent); + } + } +}