First commit. Include junk from template to remove

This commit is contained in:
Dominic Villemure
2024-03-09 20:25:30 -05:00
commit bbcefcf76f
140 changed files with 8151 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Hutopy.Application.FunctionalTests</RootNamespace>
<AssemblyName>Hutopy.Application.FunctionalTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="nunit" />
<PackageReference Include="NUnit.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="Respawn" />
<PackageReference Include="Testcontainers.MsSql" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Web\Web.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
namespace Hutopy.Application.FunctionalTests;
using static Testing;
[TestFixture]
public abstract class BaseTestFixture
{
[SetUp]
public async Task TestSetUp()
{
await ResetState();
}
}

View File

@@ -0,0 +1,42 @@
using System.Data.Common;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Infrastructure.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Hutopy.Application.FunctionalTests;
using static Testing;
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly DbConnection _connection;
public CustomWebApplicationFactory(DbConnection connection)
{
_connection = connection;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services
.RemoveAll<IUser>()
.AddTransient(provider => Mock.Of<IUser>(s => s.Id == GetUserId()));
services
.RemoveAll<DbContextOptions<ApplicationDbContext>>()
.AddDbContext<ApplicationDbContext>((sp, options) =>
{
options.AddInterceptors(sp.GetServices<ISaveChangesInterceptor>());
options.UseSqlServer(_connection);
});
});
}
}

View File

@@ -0,0 +1,4 @@
global using Ardalis.GuardClauses;
global using FluentAssertions;
global using Moq;
global using NUnit.Framework;

View File

@@ -0,0 +1,14 @@
using System.Data.Common;
namespace Hutopy.Application.FunctionalTests;
public interface ITestDatabase
{
Task InitialiseAsync();
DbConnection GetConnection();
Task ResetAsync();
Task DisposeAsync();
}

View File

@@ -0,0 +1,62 @@
using System.Data.Common;
using Hutopy.Infrastructure.Data;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Respawn;
namespace Hutopy.Application.FunctionalTests;
public class SqlServerTestDatabase : ITestDatabase
{
private readonly string _connectionString = null!;
private SqlConnection _connection = null!;
private Respawner _respawner = null!;
public SqlServerTestDatabase()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
var connectionString = configuration.GetConnectionString("DefaultConnection");
Guard.Against.Null(connectionString);
_connectionString = connectionString;
}
public async Task InitialiseAsync()
{
_connection = new SqlConnection(_connectionString);
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(_connectionString)
.Options;
var context = new ApplicationDbContext(options);
context.Database.Migrate();
_respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions
{
TablesToIgnore = new Respawn.Graph.Table[] { "__EFMigrationsHistory" }
});
}
public DbConnection GetConnection()
{
return _connection;
}
public async Task ResetAsync()
{
await _respawner.ResetAsync(_connectionString);
}
public async Task DisposeAsync()
{
await _connection.DisposeAsync();
}
}

View File

@@ -0,0 +1,13 @@
namespace Hutopy.Application.FunctionalTests;
public static class TestDatabaseFactory
{
public static async Task<ITestDatabase> CreateAsync()
{
var database = new TestcontainersTestDatabase();
await database.InitialiseAsync();
return database;
}
}

View File

@@ -0,0 +1,61 @@
using System.Data.Common;
using Hutopy.Infrastructure.Data;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Respawn;
using Testcontainers.MsSql;
namespace Hutopy.Application.FunctionalTests;
public class TestcontainersTestDatabase : ITestDatabase
{
private readonly MsSqlContainer _container;
private DbConnection _connection = null!;
private string _connectionString = null!;
private Respawner _respawner = null!;
public TestcontainersTestDatabase()
{
_container = new MsSqlBuilder()
.WithAutoRemove(true)
.Build();
}
public async Task InitialiseAsync()
{
await _container.StartAsync();
_connectionString = _container.GetConnectionString();
_connection = new SqlConnection(_connectionString);
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(_connectionString)
.Options;
var context = new ApplicationDbContext(options);
context.Database.Migrate();
_respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions
{
TablesToIgnore = new Respawn.Graph.Table[] { "__EFMigrationsHistory" }
});
}
public DbConnection GetConnection()
{
return _connection;
}
public async Task ResetAsync()
{
await _respawner.ResetAsync(_connectionString);
}
public async Task DisposeAsync()
{
await _connection.DisposeAsync();
await _container.DisposeAsync();
}
}

View File

@@ -0,0 +1,146 @@
using Hutopy.Domain.Constants;
using Hutopy.Infrastructure.Data;
using Hutopy.Infrastructure.Identity;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Hutopy.Application.FunctionalTests;
[SetUpFixture]
public partial class Testing
{
private static ITestDatabase _database;
private static CustomWebApplicationFactory _factory = null!;
private static IServiceScopeFactory _scopeFactory = null!;
private static string? _userId;
[OneTimeSetUp]
public async Task RunBeforeAnyTests()
{
_database = await TestDatabaseFactory.CreateAsync();
_factory = new CustomWebApplicationFactory(_database.GetConnection());
_scopeFactory = _factory.Services.GetRequiredService<IServiceScopeFactory>();
}
public static async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
using var scope = _scopeFactory.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<ISender>();
return await mediator.Send(request);
}
public static async Task SendAsync(IBaseRequest request)
{
using var scope = _scopeFactory.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<ISender>();
await mediator.Send(request);
}
public static string? GetUserId()
{
return _userId;
}
public static async Task<string> RunAsDefaultUserAsync()
{
return await RunAsUserAsync("test@local", "Testing1234!", Array.Empty<string>());
}
public static async Task<string> RunAsAdministratorAsync()
{
return await RunAsUserAsync("administrator@local", "Administrator1234!", new[] { Roles.Administrator });
}
public static async Task<string> RunAsUserAsync(string userName, string password, string[] roles)
{
using var scope = _scopeFactory.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var user = new ApplicationUser { UserName = userName, Email = userName };
var result = await userManager.CreateAsync(user, password);
if (roles.Any())
{
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
foreach (var role in roles)
{
await roleManager.CreateAsync(new IdentityRole(role));
}
await userManager.AddToRolesAsync(user, roles);
}
if (result.Succeeded)
{
_userId = user.Id;
return _userId;
}
var errors = string.Join(Environment.NewLine, result.ToApplicationResult().Errors);
throw new Exception($"Unable to create {userName}.{Environment.NewLine}{errors}");
}
public static async Task ResetState()
{
try
{
await _database.ResetAsync();
}
catch (Exception)
{
}
_userId = null;
}
public static async Task<TEntity?> FindAsync<TEntity>(params object[] keyValues)
where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
return await context.FindAsync<TEntity>(keyValues);
}
public static async Task AddAsync<TEntity>(TEntity entity)
where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task<int> CountAsync<TEntity>() where TEntity : class
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
return await context.Set<TEntity>().CountAsync();
}
[OneTimeTearDown]
public async Task RunAfterAnyTests()
{
await _database.DisposeAsync();
await _factory.DisposeAsync();
}
}

View File

@@ -0,0 +1,49 @@
using Hutopy.Application.Common.Exceptions;
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoItems.Commands;
using static Testing;
public class CreateTodoItemTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireMinimumFields()
{
var command = new CreateTodoItemCommand();
await FluentActions.Invoking(() =>
SendAsync(command)).Should().ThrowAsync<ValidationException>();
}
[Test]
public async Task ShouldCreateTodoItem()
{
var userId = await RunAsDefaultUserAsync();
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
var command = new CreateTodoItemCommand
{
ListId = listId,
Title = "Tasks"
};
var itemId = await SendAsync(command);
var item = await FindAsync<TodoItem>(itemId);
item.Should().NotBeNull();
item!.ListId.Should().Be(command.ListId);
item.Title.Should().Be(command.Title);
item.CreatedBy.Should().Be(userId);
item.Created.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
item.LastModifiedBy.Should().Be(userId);
item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}

View File

@@ -0,0 +1,41 @@
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
using Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoItems.Commands;
using static Testing;
public class DeleteTodoItemTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireValidTodoItemId()
{
var command = new DeleteTodoItemCommand(99);
await FluentActions.Invoking(() =>
SendAsync(command)).Should().ThrowAsync<NotFoundException>();
}
[Test]
public async Task ShouldDeleteTodoItem()
{
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
var itemId = await SendAsync(new CreateTodoItemCommand
{
ListId = listId,
Title = "New Item"
});
await SendAsync(new DeleteTodoItemCommand(itemId));
var item = await FindAsync<TodoItem>(itemId);
item.Should().BeNull();
}
}

View File

@@ -0,0 +1,57 @@
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
using Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
using Hutopy.Application.TodoItems.Commands.UpdateTodoItemDetail;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Domain.Entities;
using Hutopy.Domain.Enums;
namespace Hutopy.Application.FunctionalTests.TodoItems.Commands;
using static Testing;
public class UpdateTodoItemDetailTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireValidTodoItemId()
{
var command = new UpdateTodoItemCommand { Id = 99, Title = "New Title" };
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<NotFoundException>();
}
[Test]
public async Task ShouldUpdateTodoItem()
{
var userId = await RunAsDefaultUserAsync();
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
var itemId = await SendAsync(new CreateTodoItemCommand
{
ListId = listId,
Title = "New Item"
});
var command = new UpdateTodoItemDetailCommand
{
Id = itemId,
ListId = listId,
Note = "This is the note.",
Priority = PriorityLevel.High
};
await SendAsync(command);
var item = await FindAsync<TodoItem>(itemId);
item.Should().NotBeNull();
item!.ListId.Should().Be(command.ListId);
item.Note.Should().Be(command.Note);
item.Priority.Should().Be(command.Priority);
item.LastModifiedBy.Should().NotBeNull();
item.LastModifiedBy.Should().Be(userId);
item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}

View File

@@ -0,0 +1,51 @@
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
using Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoItems.Commands;
using static Testing;
public class UpdateTodoItemTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireValidTodoItemId()
{
var command = new UpdateTodoItemCommand { Id = 99, Title = "New Title" };
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<NotFoundException>();
}
[Test]
public async Task ShouldUpdateTodoItem()
{
var userId = await RunAsDefaultUserAsync();
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
var itemId = await SendAsync(new CreateTodoItemCommand
{
ListId = listId,
Title = "New Item"
});
var command = new UpdateTodoItemCommand
{
Id = itemId,
Title = "Updated Item Title"
};
await SendAsync(command);
var item = await FindAsync<TodoItem>(itemId);
item.Should().NotBeNull();
item!.Title.Should().Be(command.Title);
item.LastModifiedBy.Should().NotBeNull();
item.LastModifiedBy.Should().Be(userId);
item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}

View File

@@ -0,0 +1,54 @@
using Hutopy.Application.Common.Exceptions;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoLists.Commands;
using static Testing;
public class CreateTodoListTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireMinimumFields()
{
var command = new CreateTodoListCommand();
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<ValidationException>();
}
[Test]
public async Task ShouldRequireUniqueTitle()
{
await SendAsync(new CreateTodoListCommand
{
Title = "Shopping"
});
var command = new CreateTodoListCommand
{
Title = "Shopping"
};
await FluentActions.Invoking(() =>
SendAsync(command)).Should().ThrowAsync<ValidationException>();
}
[Test]
public async Task ShouldCreateTodoList()
{
var userId = await RunAsDefaultUserAsync();
var command = new CreateTodoListCommand
{
Title = "Tasks"
};
var id = await SendAsync(command);
var list = await FindAsync<TodoList>(id);
list.Should().NotBeNull();
list!.Title.Should().Be(command.Title);
list.CreatedBy.Should().Be(userId);
list.Created.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}

View File

@@ -0,0 +1,32 @@
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Application.TodoLists.Commands.DeleteTodoList;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoLists.Commands;
using static Testing;
public class DeleteTodoListTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireValidTodoListId()
{
var command = new DeleteTodoListCommand(99);
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<NotFoundException>();
}
[Test]
public async Task ShouldDeleteTodoList()
{
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
await SendAsync(new DeleteTodoListCommand(listId));
var list = await FindAsync<TodoList>(listId);
list.Should().BeNull();
}
}

View File

@@ -0,0 +1,75 @@
using Hutopy.Application.Common.Exceptions;
using Hutopy.Application.Common.Security;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Application.TodoLists.Commands.PurgeTodoLists;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoLists.Commands;
using static Testing;
public class PurgeTodoListsTests : BaseTestFixture
{
[Test]
public async Task ShouldDenyAnonymousUser()
{
var command = new PurgeTodoListsCommand();
command.GetType().Should().BeDecoratedWith<AuthorizeAttribute>();
var action = () => SendAsync(command);
await action.Should().ThrowAsync<UnauthorizedAccessException>();
}
[Test]
public async Task ShouldDenyNonAdministrator()
{
await RunAsDefaultUserAsync();
var command = new PurgeTodoListsCommand();
var action = () => SendAsync(command);
await action.Should().ThrowAsync<ForbiddenAccessException>();
}
[Test]
public async Task ShouldAllowAdministrator()
{
await RunAsAdministratorAsync();
var command = new PurgeTodoListsCommand();
var action = () => SendAsync(command);
await action.Should().NotThrowAsync<ForbiddenAccessException>();
}
[Test]
public async Task ShouldDeleteAllLists()
{
await RunAsAdministratorAsync();
await SendAsync(new CreateTodoListCommand
{
Title = "New List #1"
});
await SendAsync(new CreateTodoListCommand
{
Title = "New List #2"
});
await SendAsync(new CreateTodoListCommand
{
Title = "New List #3"
});
await SendAsync(new PurgeTodoListsCommand());
var count = await CountAsync<TodoList>();
count.Should().Be(0);
}
}

View File

@@ -0,0 +1,70 @@
using Hutopy.Application.Common.Exceptions;
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
using Hutopy.Application.TodoLists.Commands.UpdateTodoList;
using Hutopy.Domain.Entities;
namespace Hutopy.Application.FunctionalTests.TodoLists.Commands;
using static Testing;
public class UpdateTodoListTests : BaseTestFixture
{
[Test]
public async Task ShouldRequireValidTodoListId()
{
var command = new UpdateTodoListCommand { Id = 99, Title = "New Title" };
await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync<NotFoundException>();
}
[Test]
public async Task ShouldRequireUniqueTitle()
{
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
await SendAsync(new CreateTodoListCommand
{
Title = "Other List"
});
var command = new UpdateTodoListCommand
{
Id = listId,
Title = "Other List"
};
(await FluentActions.Invoking(() =>
SendAsync(command))
.Should().ThrowAsync<ValidationException>().Where(ex => ex.Errors.ContainsKey("Title")))
.And.Errors["Title"].Should().Contain("'Title' must be unique.");
}
[Test]
public async Task ShouldUpdateTodoList()
{
var userId = await RunAsDefaultUserAsync();
var listId = await SendAsync(new CreateTodoListCommand
{
Title = "New List"
});
var command = new UpdateTodoListCommand
{
Id = listId,
Title = "Updated List Title"
};
await SendAsync(command);
var list = await FindAsync<TodoList>(listId);
list.Should().NotBeNull();
list!.Title.Should().Be(command.Title);
list.LastModifiedBy.Should().NotBeNull();
list.LastModifiedBy.Should().Be(userId);
list.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
}
}

View File

@@ -0,0 +1,61 @@
using Hutopy.Application.TodoLists.Queries.GetTodos;
using Hutopy.Domain.Entities;
using Hutopy.Domain.ValueObjects;
namespace Hutopy.Application.FunctionalTests.TodoLists.Queries;
using static Testing;
public class GetTodosTests : BaseTestFixture
{
[Test]
public async Task ShouldReturnPriorityLevels()
{
await RunAsDefaultUserAsync();
var query = new GetTodosQuery();
var result = await SendAsync(query);
result.PriorityLevels.Should().NotBeEmpty();
}
[Test]
public async Task ShouldReturnAllListsAndItems()
{
await RunAsDefaultUserAsync();
await AddAsync(new TodoList
{
Title = "Shopping",
Colour = Colour.Blue,
Items =
{
new TodoItem { Title = "Apples", Done = true },
new TodoItem { Title = "Milk", Done = true },
new TodoItem { Title = "Bread", Done = true },
new TodoItem { Title = "Toilet paper" },
new TodoItem { Title = "Pasta" },
new TodoItem { Title = "Tissues" },
new TodoItem { Title = "Tuna" }
}
});
var query = new GetTodosQuery();
var result = await SendAsync(query);
result.Lists.Should().HaveCount(1);
result.Lists.First().Items.Should().HaveCount(7);
}
[Test]
public async Task ShouldDenyAnonymousUser()
{
var query = new GetTodosQuery();
var action = () => SendAsync(query);
await action.Should().ThrowAsync<UnauthorizedAccessException>();
}
}

View File

@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HutopyTestDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Hutopy.Application.UnitTests</RootNamespace>
<AssemblyName>Hutopy.Application.UnitTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="nunit" />
<PackageReference Include="NUnit.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Application\Application.csproj" />
<ProjectReference Include="..\..\src\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,45 @@
using Hutopy.Application.Common.Behaviours;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace Hutopy.Application.UnitTests.Common.Behaviours;
public class RequestLoggerTests
{
private Mock<ILogger<CreateTodoItemCommand>> _logger = null!;
private Mock<IUser> _user = null!;
private Mock<IIdentityService> _identityService = null!;
[SetUp]
public void Setup()
{
_logger = new Mock<ILogger<CreateTodoItemCommand>>();
_user = new Mock<IUser>();
_identityService = new Mock<IIdentityService>();
}
[Test]
public async Task ShouldCallGetUserNameAsyncOnceIfAuthenticated()
{
_user.Setup(x => x.Id).Returns(Guid.NewGuid().ToString());
var requestLogger = new LoggingBehaviour<CreateTodoItemCommand>(_logger.Object, _user.Object, _identityService.Object);
await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = "title" }, new CancellationToken());
_identityService.Verify(i => i.GetUserNameAsync(It.IsAny<string>()), Times.Once);
}
[Test]
public async Task ShouldNotCallGetUserNameAsyncOnceIfUnauthenticated()
{
var requestLogger = new LoggingBehaviour<CreateTodoItemCommand>(_logger.Object, _user.Object, _identityService.Object);
await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = "title" }, new CancellationToken());
_identityService.Verify(i => i.GetUserNameAsync(It.IsAny<string>()), Times.Never);
}
}

View File

@@ -0,0 +1,63 @@
using Hutopy.Application.Common.Exceptions;
using FluentAssertions;
using FluentValidation.Results;
using NUnit.Framework;
namespace Hutopy.Application.UnitTests.Common.Exceptions;
public class ValidationExceptionTests
{
[Test]
public void DefaultConstructorCreatesAnEmptyErrorDictionary()
{
var actual = new ValidationException().Errors;
actual.Keys.Should().BeEquivalentTo(Array.Empty<string>());
}
[Test]
public void SingleValidationFailureCreatesASingleElementErrorDictionary()
{
var failures = new List<ValidationFailure>
{
new ValidationFailure("Age", "must be over 18"),
};
var actual = new ValidationException(failures).Errors;
actual.Keys.Should().BeEquivalentTo(new string[] { "Age" });
actual["Age"].Should().BeEquivalentTo(new string[] { "must be over 18" });
}
[Test]
public void MulitpleValidationFailureForMultiplePropertiesCreatesAMultipleElementErrorDictionaryEachWithMultipleValues()
{
var failures = new List<ValidationFailure>
{
new ValidationFailure("Age", "must be 18 or older"),
new ValidationFailure("Age", "must be 25 or younger"),
new ValidationFailure("Password", "must contain at least 8 characters"),
new ValidationFailure("Password", "must contain a digit"),
new ValidationFailure("Password", "must contain upper case letter"),
new ValidationFailure("Password", "must contain lower case letter"),
};
var actual = new ValidationException(failures).Errors;
actual.Keys.Should().BeEquivalentTo(new string[] { "Password", "Age" });
actual["Age"].Should().BeEquivalentTo(new string[]
{
"must be 25 or younger",
"must be 18 or older",
});
actual["Password"].Should().BeEquivalentTo(new string[]
{
"must contain lower case letter",
"must contain upper case letter",
"must contain at least 8 characters",
"must contain a digit",
});
}
}

View File

@@ -0,0 +1,53 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using AutoMapper;
using Hutopy.Application.Common.Interfaces;
using Hutopy.Application.Common.Models;
using Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
using Hutopy.Application.TodoLists.Queries.GetTodos;
using Hutopy.Domain.Entities;
using NUnit.Framework;
namespace Hutopy.Application.UnitTests.Common.Mappings;
public class MappingTests
{
private readonly IConfigurationProvider _configuration;
private readonly IMapper _mapper;
public MappingTests()
{
_configuration = new MapperConfiguration(config =>
config.AddMaps(Assembly.GetAssembly(typeof(IApplicationDbContext))));
_mapper = _configuration.CreateMapper();
}
[Test]
public void ShouldHaveValidConfiguration()
{
_configuration.AssertConfigurationIsValid();
}
[Test]
[TestCase(typeof(TodoList), typeof(TodoListDto))]
[TestCase(typeof(TodoItem), typeof(TodoItemDto))]
[TestCase(typeof(TodoList), typeof(LookupDto))]
[TestCase(typeof(TodoItem), typeof(LookupDto))]
[TestCase(typeof(TodoItem), typeof(TodoItemBriefDto))]
public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination)
{
var instance = GetInstanceOf(source);
_mapper.Map(instance, source, destination);
}
private object GetInstanceOf(Type type)
{
if (type.GetConstructor(Type.EmptyTypes) != null)
return Activator.CreateInstance(type)!;
// Type without parameterless constructor
return RuntimeHelpers.GetUninitializedObject(type);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Hutopy.Domain.UnitTests</RootNamespace>
<AssemblyName>Hutopy.Domain.UnitTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="nunit" />
<PackageReference Include="NUnit.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Domain\Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
using Hutopy.Domain.Exceptions;
using Hutopy.Domain.ValueObjects;
using FluentAssertions;
using NUnit.Framework;
namespace Hutopy.Domain.UnitTests.ValueObjects;
public class ColourTests
{
[Test]
public void ShouldReturnCorrectColourCode()
{
var code = "#FFFFFF";
var colour = Colour.From(code);
colour.Code.Should().Be(code);
}
[Test]
public void ToStringReturnsCode()
{
var colour = Colour.White;
colour.ToString().Should().Be(colour.Code);
}
[Test]
public void ShouldPerformImplicitConversionToColourCodeString()
{
string code = Colour.White;
code.Should().Be("#FFFFFF");
}
[Test]
public void ShouldPerformExplicitConversionGivenSupportedColourCode()
{
var colour = (Colour)"#FFFFFF";
colour.Should().Be(Colour.White);
}
[Test]
public void ShouldThrowUnsupportedColourExceptionGivenNotSupportedColourCode()
{
FluentActions.Invoking(() => Colour.From("##FF33CC"))
.Should().Throw<UnsupportedColourException>();
}
}

View File

@@ -0,0 +1 @@
global using NUnit.Framework;

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Hutopy.Infrastructure.IntegrationTests</RootNamespace>
<AssemblyName>Hutopy.Infrastructure.IntegrationTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="NUnit.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" />
</ItemGroup>
</Project>