A Tests/NosSmooth.Core.Tests/Commands/CommandProcessorTests.cs => Tests/NosSmooth.Core.Tests/Commands/CommandProcessorTests.cs +515 -0
@@ 0,0 1,515 @@
+//
+// CommandProcessorTests.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Errors;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Tests.Fakes;
+using NosSmooth.Core.Tests.Fakes.Commands;
+using NosSmooth.Core.Tests.Fakes.Commands.Events;
+using Remora.Results;
+using Xunit;
+
+namespace NosSmooth.Core.Tests.Commands;
+
+/// <summary>
+/// Test for <see cref="CommandProcessor"/>.
+/// </summary>
+public class CommandProcessorTests
+{
+ /// <summary>
+ /// Tests that unknown not registered command should return a <see cref="CommandHandlerNotFound"/>.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task ProcessCommand_UnknownCommand_ShouldReturnError()
+ {
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), new FakeCommand("asdf"));
+ Assert.False(processResult.IsSuccess);
+ Assert.IsType<CommandHandlerNotFound>(processResult.Error);
+ }
+
+ /// <summary>
+ /// Tests that known command has its handler called.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_KnownCommand_ShouldCallHandler()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ called = true;
+ return Result.FromSuccess();
+ }
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.True(processResult.IsSuccess);
+ Assert.True(called);
+ }
+
+ /// <summary>
+ /// Tests that if there are pre events they will be called.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingPreEvents_ShouldCallPreEvents()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddPreCommandExecutionEvent<SuccessfulCommandEvent>()
+ .AddScoped<IPreCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ c =>
+ {
+ Assert.Equal(fakeCommand, c);
+ called = true;
+ return Result.FromSuccess();
+ },
+ (_, _) => throw new NotImplementedException()
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => Result.FromSuccess()
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.True(processResult.IsSuccess);
+ Assert.True(called);
+ }
+
+ /// <summary>
+ /// Tests that if there are pre events that return an error, the handler of the command won't be called.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingErrorfulPreEvents_ShouldNotCallHandler()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddPreCommandExecutionEvent<ErrorCommandEvent>()
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc =>
+ {
+ called = true;
+ return Result.FromSuccess();
+ }
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.False(processResult.IsSuccess);
+ Assert.False(called);
+ }
+
+ /// <summary>
+ /// Tests that if there are pre events that return successful result, the handler should be called.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingSuccessfulPreEvents_ShouldCallHandler()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddPreCommandExecutionEvent<SuccessfulCommandEvent>()
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ called = true;
+ return Result.FromSuccess();
+ }
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.True(processResult.IsSuccess);
+ Assert.True(called);
+ }
+
+ /// <summary>
+ /// Tests if there are post events they will be called.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingPostEvents_ShouldCallPostEvents()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPostCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new NotImplementedException(),
+ (fc, res) =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ Assert.True(res.IsSuccess);
+ called = true;
+ return Result.FromSuccess();
+ }
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => { return Result.FromSuccess(); }
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.True(processResult.IsSuccess);
+ Assert.True(called);
+ }
+
+ /// <summary>
+ /// Tests if there are post events, the successful result from the handler should be passed to them.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingPostEvents_ShouldPassSuccessfulResultToPostEvents()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPostCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new NotImplementedException(),
+ (fc, res) =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ Assert.True(res.IsSuccess);
+ called = true;
+ return Result.FromSuccess();
+ }
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => { return Result.FromSuccess(); }
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.True(processResult.IsSuccess);
+ Assert.True(called);
+ }
+
+ /// <summary>
+ /// Tests if there are post events, the error from the handler should be passed to them.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingPostEvents_ShouldPassErrorfulResultToPostEvents()
+ {
+ bool called = false;
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPostCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new NotImplementedException(),
+ (fc, res) =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ Assert.False(res.IsSuccess);
+ Assert.IsType<GenericError>(res.Error);
+ called = true;
+ return Result.FromSuccess();
+ }
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => { return new FakeError("Error"); }
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.False(processResult.IsSuccess);
+ Assert.True(called);
+ }
+
+ /// <summary>
+ /// Tests that error from post events is returned.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingErrorfulPostEvents_ShouldReturnPostExecutionError()
+ {
+ var fakeCommand = new FakeCommand("asdf");
+ var error = new FakeError("Error");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPostCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new NotImplementedException(),
+ (fc, _) =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ return error;
+ }
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => Result.FromSuccess()
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.False(processResult.IsSuccess);
+ Assert.Equal(error, processResult.Error);
+ }
+
+ /// <summary>
+ /// Tests that error from pre events is returned.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingErrorfulPreEvents_ShouldReturnPreExecutionError()
+ {
+ var fakeCommand = new FakeCommand("asdf");
+ var error = new FakeError("Error");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPreCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ (fc) =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ return error;
+ },
+ (_, _) => throw new NotImplementedException()
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => Result.FromSuccess()
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.False(processResult.IsSuccess);
+ Assert.Equal(error, processResult.Error);
+ }
+
+ /// <summary>
+ /// Tests that error from post event and handler is returned.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingErrorfulPostEventsAndHandler_ShouldReturnHandlerAndPostExecutionError()
+ {
+ var fakeCommand = new FakeCommand("asdf");
+ var error1 = new FakeError();
+ var error2 = new FakeError();
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPostCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new NotImplementedException(),
+ (fc, _) =>
+ {
+ Assert.Equal(fakeCommand, fc);
+ return error1;
+ }
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => error2
+ )
+ )
+ .BuildServiceProvider();
+
+ var processResult = await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ Assert.False(processResult.IsSuccess);
+ Assert.IsType<AggregateError>(processResult.Error);
+ var aggregateError = processResult.Error as AggregateError;
+ Assert.NotNull(aggregateError);
+ if (aggregateError is not null)
+ {
+ Assert.True(aggregateError.Errors.Any(x => x.Error == error1));
+
+ Assert.True(aggregateError.Errors.Any(x => x.Error == error2));
+ Assert.Equal(2, aggregateError.Errors.Count);
+ }
+ }
+
+ /// <summary>
+ /// Tests that exceptions are handled.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingExceptionInHandler_ShouldNotThrow()
+ {
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPostCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new Exception(),
+ (_, _) => throw new Exception()
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => throw new Exception()
+ )
+ )
+ .BuildServiceProvider();
+
+ await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ }
+
+ /// <summary>
+ /// Tests that exceptions are handled.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingExceptionInPreEvent_ShouldNotThrow()
+ {
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPreCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => throw new Exception(),
+ (_, _) => throw new Exception()
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => throw new Exception()
+ )
+ )
+ .BuildServiceProvider();
+
+ await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ }
+
+ /// <summary>
+ /// Tests that exceptions are handled.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task ProcessCommand_HavingExceptionInPostEvent_ShouldNotThrow()
+ {
+ var fakeCommand = new FakeCommand("asdf");
+ var provider = new ServiceCollection()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<IPreCommandExecutionEvent>
+ (
+ _ => new CommandEvent<FakeCommand>
+ (
+ _ => Result.FromSuccess(),
+ (_, _) => throw new Exception()
+ )
+ )
+ .AddScoped<ICommandHandler<FakeCommand>, FakeCommandHandler>
+ (
+ _ => new FakeCommandHandler
+ (
+ fc => Result.FromSuccess()
+ )
+ )
+ .BuildServiceProvider();
+
+ await provider.GetRequiredService<CommandProcessor>().ProcessCommand
+ (new FakeEmptyNostaleClient(), fakeCommand);
+ }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs => Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs +213 -0
@@ 0,0 1,213 @@
+//
+// WalkCommandHandlerTests.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using NosSmooth.Core.Commands.Control;
+using NosSmooth.Core.Commands.Walking;
+using NosSmooth.Core.Tests.Fakes;
+using Remora.Results;
+using Xunit;
+
+namespace NosSmooth.Core.Tests.Commands.Walking;
+
+/// <summary>
+/// Tests handling walk command.
+/// </summary>
+public class WalkCommandHandlerTests
+{
+ /// <summary>
+ /// Tests that pet and player walk commands will be called.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task Handle_CallsPetAndPlayerWalkCommands()
+ {
+ var calledPetWalk = false;
+ var calledPlayerWalk = false;
+ var command = new WalkCommand(0, 0, new[] { 1, 2 }, 0);
+ var walkHandler = new WalkCommandHandler
+ (
+ new FakeNostaleClient
+ (
+ (c, _) =>
+ {
+ if (c is PlayerWalkCommand)
+ {
+ calledPlayerWalk = true;
+ }
+ if (c is PetWalkCommand)
+ {
+ calledPetWalk = true;
+ }
+ return Result.FromSuccess();
+ }
+ )
+ );
+
+ await walkHandler.HandleCommand(command);
+ Assert.True(calledPetWalk);
+ Assert.True(calledPlayerWalk);
+ }
+
+ /// <summary>
+ /// Tests that handling will preserve the <see cref="ITakeControlCommand"/> properties.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task Handle_PreservesTakeHandlerCommandProperties()
+ {
+ var command = new WalkCommand
+ (
+ 0,
+ 0,
+ new[] { 2, 5, 7, 9 },
+ 0,
+ true,
+ false,
+ false
+ );
+ var walkHandler = new WalkCommandHandler
+ (
+ new FakeNostaleClient
+ (
+ (c, _) =>
+ {
+ if (c is ITakeControlCommand takeControl)
+ {
+ Assert.Equal(command.AllowUserCancel, takeControl.AllowUserCancel);
+ Assert.Equal(command.WaitForCancellation, takeControl.WaitForCancellation);
+ Assert.Equal(command.CancelOnMapChange, takeControl.CancelOnMapChange);
+ Assert.Equal(command.CanBeCancelledByAnother, takeControl.CanBeCancelledByAnother);
+ }
+ return Result.FromSuccess();
+ }
+ )
+ );
+
+ await walkHandler.HandleCommand(command);
+ }
+
+ /// <summary>
+ /// Tests that handler preserves the position to player walk command.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task Handle_PreservesPlayerWalkPosition()
+ {
+ var command = new WalkCommand
+ (
+ 10,
+ 15,
+ Array.Empty<int>(),
+ 0,
+ true,
+ false,
+ false
+ );
+ var walkHandler = new WalkCommandHandler
+ (
+ new FakeNostaleClient
+ (
+ (c, _) =>
+ {
+ if (c is PlayerWalkCommand playerWalkCommand)
+ {
+ Assert.Equal(command.TargetX, playerWalkCommand.TargetX);
+ Assert.Equal(command.TargetY, playerWalkCommand.TargetY);
+ Assert.Equal(command.ReturnDistanceTolerance, playerWalkCommand.ReturnDistanceTolerance);
+ }
+ return Result.FromSuccess();
+ }
+ )
+ );
+
+ await walkHandler.HandleCommand(command);
+ }
+
+ /// <summary>
+ /// Tests that the handler will be called for every pet.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task Handle_WithPets_IsCalledForEveryPet()
+ {
+ var calledCount = 0;
+ var command = new WalkCommand
+ (
+ 10,
+ 15,
+ new[] { 1, 2, 5, 7, 8 },
+ 0,
+ true,
+ false,
+ false
+ );
+ var walkHandler = new WalkCommandHandler
+ (
+ new FakeNostaleClient
+ (
+ (c, _) =>
+ {
+ if (c is PetWalkCommand petWalkCommand)
+ {
+ if (command.PetSelectors.Contains(petWalkCommand.PetSelector))
+ {
+ calledCount++;
+ }
+ else
+ {
+ throw new ArgumentException("Pet command was called for non-selected pet.");
+ }
+ }
+ return Result.FromSuccess();
+ }
+ )
+ );
+
+ await walkHandler.HandleCommand(command);
+ Assert.Equal(command.PetSelectors.Length, calledCount);
+ }
+
+ /// <summary>
+ /// Tests that pet commands will have correct position set.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task Handle_WithPets_UsesNearbyPositionForPetCommands()
+ {
+ var command = new WalkCommand
+ (
+ 10,
+ 15,
+ new[] { 1, 2, 5, 7, 8 },
+ 0,
+ true,
+ false,
+ false
+ );
+ var walkHandler = new WalkCommandHandler
+ (
+ new FakeNostaleClient
+ (
+ (c, _) =>
+ {
+ if (c is PetWalkCommand petWalkCommand)
+ {
+ Assert.True((command.TargetX - petWalkCommand.TargetX) <= 3);
+ Assert.True((command.TargetY - petWalkCommand.TargetY) <= 3);
+ Assert.Equal(command.ReturnDistanceTolerance, petWalkCommand.ReturnDistanceTolerance);
+ }
+ return Result.FromSuccess();
+ }
+ )
+ );
+
+ await walkHandler.HandleCommand(command);
+ }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Commands/Events/CommandEvent.cs => Tests/NosSmooth.Core.Tests/Fakes/Commands/Events/CommandEvent.cs +54 -0
@@ 0,0 1,54 @@
+//
+// CommandEvent.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes.Commands.Events;
+
+/// <inheritdoc />
+public class CommandEvent<TInCommand> : IPreCommandExecutionEvent, IPostCommandExecutionEvent
+ where TInCommand : ICommand
+{
+ private readonly Func<TInCommand, Result> _preExecutionHandler;
+ private readonly Func<TInCommand, Result, Result> _postExecutionHandler;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CommandEvent{TCommand}"/> class.
+ /// </summary>
+ /// <param name="preExecutionHandler">The pre execution handler.</param>
+ /// <param name="postExecutionHandler">The post execution handler.</param>
+ public CommandEvent(Func<TInCommand, Result> preExecutionHandler, Func<TInCommand, Result, Result> postExecutionHandler)
+ {
+ _preExecutionHandler = preExecutionHandler;
+ _postExecutionHandler = postExecutionHandler;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ExecuteBeforeCommandAsync<TCommand>
+ (INostaleClient client, TCommand command, CancellationToken ct = default)
+ where TCommand : ICommand
+ {
+ return Task.FromResult(_preExecutionHandler((TInCommand)(object)command));
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ExecuteAfterCommandAsync<TCommand>
+ (
+ INostaleClient client,
+ TCommand command,
+ Result handlerResult,
+ CancellationToken ct = default
+ )
+ where TCommand : ICommand
+ {
+ return Task.FromResult(_postExecutionHandler((TInCommand)(object)command, handlerResult));
+ }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Commands/Events/ErrorCommandEvent.cs => Tests/NosSmooth.Core.Tests/Fakes/Commands/Events/ErrorCommandEvent.cs +38 -0
@@ 0,0 1,38 @@
+//
+// ErrorCommandEvent.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes.Commands.Events;
+
+/// <inheritdoc />
+public class ErrorCommandEvent : IPreCommandExecutionEvent, IPostCommandExecutionEvent
+{
+ /// <inheritdoc />
+ public Task<Result> ExecuteBeforeCommandAsync<TCommand>
+ (INostaleClient client, TCommand command, CancellationToken ct = default)
+ where TCommand : ICommand
+ {
+ return Task.FromResult<Result>(new FakeError("Error pre command execution"));
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ExecuteAfterCommandAsync<TCommand>
+ (
+ INostaleClient client,
+ TCommand command,
+ Result handlerResult,
+ CancellationToken ct = default
+ )
+ where TCommand : ICommand
+ {
+ return Task.FromResult<Result>(new FakeError("Erro post command execution"));
+ }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Commands/Events/SuccessfulCommandEvent.cs => Tests/NosSmooth.Core.Tests/Fakes/Commands/Events/SuccessfulCommandEvent.cs +41 -0
@@ 0,0 1,41 @@
+//
+// SuccessfulCommandEvent.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes.Commands.Events;
+
+/// <inheritdoc />
+public class SuccessfulCommandEvent : IPreCommandExecutionEvent, IPostCommandExecutionEvent
+{
+ /// <inheritdoc />
+ public Task<Result> ExecuteBeforeCommandAsync<TCommand>
+ (INostaleClient client, TCommand command, CancellationToken ct = default)
+ where TCommand : ICommand
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ExecuteAfterCommandAsync<TCommand>
+ (
+ INostaleClient client,
+ TCommand command,
+ Result handlerResult,
+ CancellationToken ct = default
+ )
+ where TCommand : ICommand
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommand.cs => Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommand.cs +11 -0
@@ 0,0 1,11 @@
+//
+// FakeCommand.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Commands;
+
+namespace NosSmooth.Core.Tests.Fakes.Commands;
+
+public record FakeCommand(string Input) : ICommand;<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommandHandler.cs => Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommandHandler.cs +33 -0
@@ 0,0 1,33 @@
+//
+// FakeCommandHandler.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Commands;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes.Commands;
+
+/// <inheritdoc />
+public class FakeCommandHandler : ICommandHandler<FakeCommand>
+{
+ private readonly Func<FakeCommand, Result> _handler;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FakeCommandHandler"/> class.
+ /// </summary>
+ /// <param name="handler">The handler.</param>
+ public FakeCommandHandler(Func<FakeCommand, Result> handler)
+ {
+ _handler = handler;
+
+ }
+
+ /// <inheritdoc />
+ public Task<Result> HandleCommand(FakeCommand command, CancellationToken ct = default)
+ => Task.FromResult(_handler(command));
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/FakeEmptyNostaleClient.cs => Tests/NosSmooth.Core.Tests/Fakes/FakeEmptyNostaleClient.cs +55 -0
@@ 0,0 1,55 @@
+//
+// FakeEmptyNostaleClient.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes;
+
+/// <inheritdoc />
+public class FakeEmptyNostaleClient : INostaleClient
+{
+ /// <inheritdoc />
+ public Task<Result> RunAsync(CancellationToken stopRequested = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/FakeEntity.cs => Tests/NosSmooth.Core.Tests/Fakes/FakeEntity.cs +16 -0
@@ 0,0 1,16 @@
+//
+// FakeEntity.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Stateful;
+
+namespace NosSmooth.Core.Tests.Fakes;
+
+/// <summary>
+/// A fake stateful entity.
+/// </summary>
+public class FakeEntity : IStatefulEntity
+{
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/FakeError.cs => Tests/NosSmooth.Core.Tests/Fakes/FakeError.cs +16 -0
@@ 0,0 1,16 @@
+//
+// FakeError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes;
+
+/// <summary>
+/// A fake error.
+/// </summary>
+/// <param name="Text">The text.</param>
+public record FakeError(string Text = "Fake") : ResultError($"Fake error: {Text}");<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/FakeNostaleClient.cs => Tests/NosSmooth.Core.Tests/Fakes/FakeNostaleClient.cs +66 -0
@@ 0,0 1,66 @@
+//
+// FakeNostaleClient.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes;
+
+/// <summary>
+/// Fake NosTale client.
+/// </summary>
+public class FakeNostaleClient : INostaleClient
+{
+ private readonly Func<ICommand, CancellationToken, Result> _handleCommand;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FakeNostaleClient"/> class.
+ /// </summary>
+ /// <param name="handleCommand">The handler for <see cref="SendCommandAsync"/>.</param>
+ public FakeNostaleClient(Func<ICommand, CancellationToken, Result> handleCommand)
+ {
+ _handleCommand = handleCommand;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> RunAsync(CancellationToken stopRequested = default)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default)
+ => Task.FromResult(_handleCommand(command, ct));
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Packets/FakePacket.cs => Tests/NosSmooth.Core.Tests/Fakes/Packets/FakePacket.cs +15 -0
@@ 0,0 1,15 @@
+//
+// FakePacket.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Packets;
+
+namespace NosSmooth.Core.Tests.Fakes.Packets;
+
+/// <summary>
+/// A fake packet.
+/// </summary>
+/// <param name="Input">The input.</param>
+public record FakePacket(string Input) : IPacket;<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Fakes/Packets/FakePacketHandler.cs => Tests/NosSmooth.Core.Tests/Fakes/Packets/FakePacketHandler.cs +37 -0
@@ 0,0 1,37 @@
+//
+// FakePacketHandler.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace NosSmooth.Core.Tests.Fakes.Packets;
+
+/// <summary>
+/// Fake Responder of a packet.
+/// </summary>
+/// <typeparam name="TPacket">The packet to respond to.</typeparam>
+public class FakePacketResponder<TPacket> : IPacketResponder<TPacket>
+ where TPacket : IPacket
+{
+ private readonly Func<PacketEventArgs<TPacket>, Result> _handler;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FakePacketResponder{TPacket}"/> class.
+ /// </summary>
+ /// <param name="handler">The function respond handler.</param>
+ public FakePacketResponder(Func<PacketEventArgs<TPacket>, Result> handler)
+ {
+ _handler = handler;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
+ => Task.FromResult(_handler(packetArgs));
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/IsExternalInit.cs => Tests/NosSmooth.Core.Tests/IsExternalInit.cs +16 -0
@@ 0,0 1,16 @@
+//
+// IsExternalInit.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+// ReSharper disable once CheckNamespace
+namespace System.Runtime.CompilerServices
+{
+ /// <summary>
+ /// Dummy.
+ /// </summary>
+ public class IsExternalInit
+ {
+ }
+}<
\ No newline at end of file
M Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj => Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj +1 -0
@@ 8,6 8,7 @@
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
A Tests/NosSmooth.Core.Tests/Stateful/StatefulInjectorTests.cs => Tests/NosSmooth.Core.Tests/Stateful/StatefulInjectorTests.cs +171 -0
@@ 0,0 1,171 @@
+//
+// StatefulInjectorTests.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Core.Stateful;
+using NosSmooth.Core.Tests.Fakes;
+using NosSmooth.Core.Tests.Fakes.Commands;
+using NosSmooth.Core.Tests.Fakes.Packets;
+using NosSmooth.Core.Tests.Packets;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+using Xunit;
+
+namespace NosSmooth.Core.Tests.Stateful;
+
+/// <summary>
+/// Tests injecting stateful entities.
+/// </summary>
+public class StatefulInjectorTests
+{
+ /// <summary>
+ /// Tests that get entity returns the same instance for the same INostaleClient.
+ /// </summary>
+ [Fact]
+ public void GetEntity_ReturnsSameEntityForSameClient()
+ {
+ var services = new ServiceCollection()
+ .AddSingleton<StatefulInjector>()
+ .AddSingleton<StatefulRepository>()
+ .AddSingleton<INostaleClient, FakeEmptyNostaleClient>()
+ .BuildServiceProvider();
+ var injector = new StatefulInjector(new StatefulRepository());
+ var client = services.GetRequiredService<INostaleClient>();
+ injector.Client = client;
+ var entity = injector.GetEntity(services, typeof(FakeEntity));
+ var entity2 = injector.GetEntity(services, typeof(FakeEntity));
+ Assert.Equal(entity, entity2);
+ }
+
+ /// <summary>
+ /// Tests that get entity returns different instance for different INostaleClient.
+ /// </summary>
+ [Fact]
+ public void GetEntity_ReturnsDifferentEntityForDifferentClient()
+ {
+ var services = new ServiceCollection()
+ .AddSingleton<StatefulInjector>()
+ .AddSingleton<StatefulRepository>()
+ .BuildServiceProvider();
+ var repository = new StatefulRepository();
+ var injector = new StatefulInjector(repository);
+ var injector2 = new StatefulInjector(repository);
+ var client = new FakeEmptyNostaleClient();
+ var client2 = new FakeEmptyNostaleClient();
+ injector.Client = client;
+ injector2.Client = client2;
+ var entity = injector.GetEntity(services, typeof(FakeEntity));
+ var entity2 = injector2.GetEntity(services, typeof(FakeEntity));
+ Assert.NotEqual(entity, entity2);
+ }
+
+ /// <summary>
+ /// Tests that extension methods for service provider work correctly with injectable entities, correctly adding pre event to command processor.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task CommandProcessor_PreEvent_InjectsClient()
+ {
+ var client1 = new FakeEmptyNostaleClient();
+ var client2 = new FakeEmptyNostaleClient();
+ FakeEntity? entity1 = null;
+ FakeEntity? entity2 = null;
+ var services =
+ new ServiceCollection()
+ .AddSingleton<INostaleClient, FakeEmptyNostaleClient>()
+ .AddStatefulInjector()
+ .AddStatefulEntity<FakeEntity>()
+ .AddSingleton<CommandProcessor>()
+ .AddScoped<ICommandHandler<FakeCommand>>
+ (p =>
+ {
+ var client = p.GetRequiredService<INostaleClient>();
+ var entity = p.GetRequiredService<FakeEntity>();
+ return new FakeCommandHandler((c) =>
+ {
+ if (c.Input == "1")
+ {
+ Assert.Equal(client1, client);
+ entity1 = entity;
+ }
+ else
+ {
+ Assert.Equal(client2, client);
+ entity2 = entity;
+ }
+ return Result.FromSuccess();
+ }
+ );
+ }
+ )
+ .BuildServiceProvider();
+
+ var processor = services.GetRequiredService<CommandProcessor>();
+
+ Assert.True((await processor.ProcessCommand(client1, new FakeCommand("1"), default)).IsSuccess);
+ Assert.True((await processor.ProcessCommand(client2, new FakeCommand("2"), default)).IsSuccess);
+ Assert.NotNull(entity1);
+ Assert.NotNull(entity2);
+ Assert.NotEqual(entity1, entity2);
+ }
+
+ /// <summary>
+ /// Tests that extension methods for service provider work correctly with injectable entities, correctly adding pre event to packet handler.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
+ [Fact]
+ public async Task PacketHandler_PreEvent_InjectsClient()
+ {
+ var client1 = new FakeEmptyNostaleClient();
+ var client2 = new FakeEmptyNostaleClient();
+ FakeEntity? entity1 = null;
+ FakeEntity? entity2 = null;
+ var services =
+ new ServiceCollection()
+ .AddSingleton<INostaleClient, FakeEmptyNostaleClient>()
+ .AddStatefulInjector()
+ .AddStatefulEntity<FakeEntity>()
+ .AddSingleton<CommandProcessor>()
+ .AddSingleton<PacketHandler>()
+ .AddScoped<IPacketResponder<FakePacket>>
+ (p =>
+ {
+ var client = p.GetRequiredService<INostaleClient>();
+ var entity = p.GetRequiredService<FakeEntity>();
+ return new FakePacketResponder<FakePacket>
+ ((c) =>
+ {
+ if (c.Packet.Input == "1")
+ {
+ Assert.Equal(client1, client);
+ entity1 = entity;
+ }
+ else
+ {
+ Assert.Equal(client2, client);
+ entity2 = entity;
+ }
+ return Result.FromSuccess();
+ }
+ );
+ }
+ )
+ .BuildServiceProvider();
+
+ var handler = services.GetRequiredService<PacketHandler>();
+
+ Assert.True((await handler.HandleReceivedPacketAsync(client1, new FakePacket("1"), "fake 1")).IsSuccess);
+ Assert.True((await handler.HandleReceivedPacketAsync(client2, new FakePacket("2"), "fake 2")).IsSuccess);
+ Assert.NotNull(entity1);
+ Assert.NotNull(entity2);
+ Assert.NotEqual(entity1, entity2);
+ }
+}<
\ No newline at end of file