First commit. Include junk from template to remove
This commit is contained in:
@@ -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>
|
||||
13
tests/Application.FunctionalTests/BaseTestFixture.cs
Normal file
13
tests/Application.FunctionalTests/BaseTestFixture.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Hutopy.Application.FunctionalTests;
|
||||
|
||||
using static Testing;
|
||||
|
||||
[TestFixture]
|
||||
public abstract class BaseTestFixture
|
||||
{
|
||||
[SetUp]
|
||||
public async Task TestSetUp()
|
||||
{
|
||||
await ResetState();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
4
tests/Application.FunctionalTests/GlobalUsings.cs
Normal file
4
tests/Application.FunctionalTests/GlobalUsings.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
global using FluentAssertions;
|
||||
global using Moq;
|
||||
global using NUnit.Framework;
|
||||
14
tests/Application.FunctionalTests/ITestDatabase.cs
Normal file
14
tests/Application.FunctionalTests/ITestDatabase.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Data.Common;
|
||||
|
||||
namespace Hutopy.Application.FunctionalTests;
|
||||
|
||||
public interface ITestDatabase
|
||||
{
|
||||
Task InitialiseAsync();
|
||||
|
||||
DbConnection GetConnection();
|
||||
|
||||
Task ResetAsync();
|
||||
|
||||
Task DisposeAsync();
|
||||
}
|
||||
62
tests/Application.FunctionalTests/SqlServerTestDatabase.cs
Normal file
62
tests/Application.FunctionalTests/SqlServerTestDatabase.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
13
tests/Application.FunctionalTests/TestDatabaseFactory.cs
Normal file
13
tests/Application.FunctionalTests/TestDatabaseFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
146
tests/Application.FunctionalTests/Testing.cs
Normal file
146
tests/Application.FunctionalTests/Testing.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
5
tests/Application.FunctionalTests/appsettings.json
Normal file
5
tests/Application.FunctionalTests/appsettings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HutopyTestDb;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
}
|
||||
}
|
||||
26
tests/Application.UnitTests/Application.UnitTests.csproj
Normal file
26
tests/Application.UnitTests/Application.UnitTests.csproj
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
53
tests/Application.UnitTests/Common/Mappings/MappingTests.cs
Normal file
53
tests/Application.UnitTests/Common/Mappings/MappingTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
24
tests/Domain.UnitTests/Domain.UnitTests.csproj
Normal file
24
tests/Domain.UnitTests/Domain.UnitTests.csproj
Normal 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>
|
||||
50
tests/Domain.UnitTests/ValueObjects/ColourTests.cs
Normal file
50
tests/Domain.UnitTests/ValueObjects/ColourTests.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
1
tests/Infrastructure.IntegrationTests/GlobalUsings.cs
Normal file
1
tests/Infrastructure.IntegrationTests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using NUnit.Framework;
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user