~ruther/NosSmooth

014257996adc6b4f81059146f4baebd3c89115ed — František Boháček 3 years ago 432d771
feat(tests): add tests for command processor, walk command handler, stateful injector
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

Do not follow this link