first commit
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Test dependencies">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" PrivateAssets="all" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.10.0" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="all" />
|
||||
<PackageReference Include="coverlet.collector" Version="10.0.1" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Dotnet.Samples.AspNetCore.WebApi\Dotnet.Samples.AspNetCore.WebApi.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SatelliteResourceLanguages>en-US</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,186 @@
|
||||
using System.Data.Common;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Data;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for <see cref="Repository{T}"/> and <see cref="PlayerRepository"/>.
|
||||
/// Each test runs against an in-memory SQLite database with the full EF Core migration
|
||||
/// chain applied via <see cref="DatabaseFakes.MigrateAsync"/>, which also validates
|
||||
/// that the migration chain itself is healthy as a side effect.
|
||||
/// </summary>
|
||||
public class PlayerRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private DbConnection _connection = default!;
|
||||
private PlayerDbContext _dbContext = default!;
|
||||
private PlayerRepository _repository = default!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var (connection, options) = DatabaseFakes.CreateSqliteConnection();
|
||||
_connection = connection;
|
||||
_dbContext = DatabaseFakes.CreateDbContext(options);
|
||||
await _dbContext.MigrateAsync();
|
||||
_repository = new PlayerRepository(_dbContext);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _dbContext.DisposeAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* GetAllAsync
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task GetAllAsync_WhenCalled_ReturnsAllSeededPlayers()
|
||||
{
|
||||
// Act
|
||||
var players = await _repository.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
players.Should().HaveCount(26);
|
||||
_dbContext.ChangeTracker.Entries<Player>().Should().BeEmpty();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* FindByIdAsync
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task FindByIdAsync_ExistingId_ReturnsPlayer()
|
||||
{
|
||||
// Arrange — resolve a real ID from the seeded database
|
||||
var seeded = await _repository.GetAllAsync();
|
||||
var existingId = seeded[0].Id;
|
||||
|
||||
// Act
|
||||
var player = await _repository.FindByIdAsync(existingId);
|
||||
|
||||
// Assert
|
||||
player.Should().NotBeNull();
|
||||
player!.Id.Should().Be(existingId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task FindByIdAsync_UnknownId_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var player = await _repository.FindByIdAsync(Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
player.Should().BeNull();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* RemoveAsync
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task RemoveAsync_ExistingEntity_RemovesFromDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var seeded = await _repository.GetAllAsync();
|
||||
var existingId = seeded[0].Id;
|
||||
|
||||
// Act
|
||||
await _repository.RemoveAsync(existingId);
|
||||
|
||||
// Assert
|
||||
var player = await _repository.FindByIdAsync(existingId);
|
||||
player.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task RemoveAsync_UnknownId_NoExceptionThrown()
|
||||
{
|
||||
// Arrange
|
||||
var countBefore = (await _repository.GetAllAsync()).Count;
|
||||
|
||||
// Act
|
||||
var act = async () => await _repository.RemoveAsync(Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
var countAfter = (await _repository.GetAllAsync()).Count;
|
||||
countAfter.Should().Be(countBefore);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* FindBySquadNumberAsync
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task FindBySquadNumberAsync_ExistingSquadNumber_ReturnsPlayer()
|
||||
{
|
||||
// Arrange
|
||||
var expected = PlayerFakes.MakeFromStarting11(23);
|
||||
|
||||
// Act
|
||||
var player = await _repository.FindBySquadNumberAsync(expected.SquadNumber);
|
||||
|
||||
// Assert
|
||||
player.Should().NotBeNull();
|
||||
player!.SquadNumber.Should().Be(expected.SquadNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task FindBySquadNumberAsync_UnknownSquadNumber_ReturnsNull()
|
||||
{
|
||||
// Arrange — derive a squad number guaranteed not to exist in the seeded data
|
||||
var seeded = await _repository.GetAllAsync();
|
||||
var unknownSquadNumber = seeded.Max(p => p.SquadNumber) + 1;
|
||||
|
||||
// Act
|
||||
var player = await _repository.FindBySquadNumberAsync(unknownSquadNumber);
|
||||
|
||||
// Assert
|
||||
player.Should().BeNull();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* SquadNumberExistsAsync
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task SquadNumberExistsAsync_ExistingSquadNumber_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var expected = PlayerFakes.MakeFromStarting11(23);
|
||||
|
||||
// Act
|
||||
var exists = await _repository.SquadNumberExistsAsync(expected.SquadNumber);
|
||||
|
||||
// Assert
|
||||
exists.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task SquadNumberExistsAsync_UnknownSquadNumber_ReturnsFalse()
|
||||
{
|
||||
// Arrange — derive a squad number guaranteed not to exist in the seeded data
|
||||
var seeded = await _repository.GetAllAsync();
|
||||
var unknownSquadNumber = seeded.Max(p => p.SquadNumber) + 1;
|
||||
|
||||
// Act
|
||||
var exists = await _repository.SquadNumberExistsAsync(unknownSquadNumber);
|
||||
|
||||
// Assert
|
||||
exists.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using System.Data.Common;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Data;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Migrations;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the player endpoints exposed by the web application.
|
||||
/// Each test exercises the full ASP.NET Core request pipeline — routing,
|
||||
/// middleware, validation, serialization — via <see cref="WebApplicationFactory{TEntryPoint}"/>
|
||||
/// backed by an in-memory SQLite database. The factory is created fresh per test
|
||||
/// (via <see cref="IAsyncLifetime"/>) to ensure state isolation.
|
||||
/// </summary>
|
||||
public class PlayerWebApplicationTests : IAsyncLifetime
|
||||
{
|
||||
private WebApplicationFactory<Program> _factory = default!;
|
||||
private WebApplicationFactory<Program> _authorizedFactory = default!;
|
||||
private HttpClient _client = default!;
|
||||
private HttpClient _authorizedClient = default!;
|
||||
private DbConnection _connection = default!;
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
var (connection, _) = DatabaseFakes.CreateSqliteConnection();
|
||||
_connection = connection;
|
||||
|
||||
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
// AddDbContextPool registers three service types. All three must be
|
||||
// removed before re-registering with AddDbContext; leaving any one
|
||||
// causes startup to fail when the pool tries to resolve its options.
|
||||
var descriptors = services
|
||||
.Where(d =>
|
||||
d.ServiceType == typeof(DbContextOptions<PlayerDbContext>)
|
||||
|| d.ServiceType == typeof(PlayerDbContext)
|
||||
|| (
|
||||
d.ServiceType.IsGenericType
|
||||
&& d.ServiceType.GetGenericArguments().Length > 0
|
||||
&& d.ServiceType.GetGenericArguments()[0] == typeof(PlayerDbContext)
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
services.Remove(descriptor);
|
||||
|
||||
services.AddDbContext<PlayerDbContext>(options =>
|
||||
options
|
||||
.UseSqlite(_connection)
|
||||
.ReplaceService<IMigrationsAssembly, ProviderSpecificMigrationsAssembly>()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
_client = _factory.CreateClient();
|
||||
|
||||
// Derived factory that layers authentication on top of the base factory.
|
||||
// Used only by tests targeting endpoints protected with [Authorize].
|
||||
// Keeping auth out of the base factory means unauthenticated tests will
|
||||
// fail if an endpoint accidentally becomes protected.
|
||||
_authorizedFactory = _factory.WithWebHostBuilder(builder =>
|
||||
builder.ConfigureTestServices(services =>
|
||||
services
|
||||
.AddAuthentication(TestAuthHandler.SchemeName)
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
|
||||
TestAuthHandler.SchemeName,
|
||||
_ => { }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
_authorizedClient = _authorizedFactory.CreateClient();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_authorizedClient.Dispose();
|
||||
_client.Dispose();
|
||||
await _authorizedFactory.DisposeAsync();
|
||||
await _factory.DisposeAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* GET /players
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Get_Players_Existing_Returns200Ok()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/players");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var players = await response.Content.ReadFromJsonAsync<List<PlayerResponseModel>>();
|
||||
players.Should().HaveCount(26);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* GET /players/{id:Guid}
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Get_PlayerById_Existing_Returns200Ok()
|
||||
{
|
||||
// Arrange — resolve a real ID from the seeded database
|
||||
await using var scope = _authorizedFactory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<PlayerDbContext>();
|
||||
var existingId = (await db.Players.FirstAsync()).Id;
|
||||
|
||||
// Act
|
||||
var response = await _authorizedClient.GetAsync($"/players/{existingId}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var player = await response.Content.ReadFromJsonAsync<PlayerResponseModel>();
|
||||
player.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Get_PlayerById_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Act
|
||||
var response = await _authorizedClient.GetAsync($"/players/{Guid.NewGuid()}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status404NotFound);
|
||||
problem.Title.Should().Be("Not Found");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* GET /players/squadNumber/{squadNumber:int}
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Get_PlayerBySquadNumber_Existing_Returns200Ok()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/players/squadNumber/23");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var player = await response.Content.ReadFromJsonAsync<PlayerResponseModel>();
|
||||
player.Should().NotBeNull();
|
||||
player!.Dorsal.Should().Be(23);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Get_PlayerBySquadNumber_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/players/squadNumber/999");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status404NotFound);
|
||||
problem.Title.Should().Be("Not Found");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* POST /players
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Post_Players_Nonexistent_Returns201Created()
|
||||
{
|
||||
// Arrange — squad number 27 (Lo Celso) is not in the seeded data
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/players", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var player = await response.Content.ReadFromJsonAsync<PlayerResponseModel>();
|
||||
player.Should().NotBeNull();
|
||||
player!.Dorsal.Should().Be(request.SquadNumber);
|
||||
}
|
||||
|
||||
// Note: the controller's 409 Conflict branch (squad number already exists) is
|
||||
// unreachable via the HTTP pipeline. The "Create" validation rule set includes
|
||||
// BeUniqueSquadNumber, which returns a 422 validation error (Unprocessable Entity)
|
||||
// before the controller's own duplicate check ever runs. The 409 path is covered
|
||||
// by the unit test Post_Players_Existing_Returns409Conflict, where validation is
|
||||
// mocked to pass so the controller logic can be exercised in isolation.
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Post_Players_ValidationError_Returns422UnprocessableEntity()
|
||||
{
|
||||
// Arrange — SquadNumber 0 is the int default, fails NotEmpty
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.SquadNumber = 0;
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/players", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status422UnprocessableEntity);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* PUT /players/squadNumber/{squadNumber:int}
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Put_PlayerBySquadNumber_Existing_Returns204NoContent()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(23);
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Put_PlayerBySquadNumber_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Arrange — squad number 999 does not exist; body matches route to avoid mismatch error
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(23);
|
||||
request.SquadNumber = 999;
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync("/players/squadNumber/999", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status404NotFound);
|
||||
problem.Title.Should().Be("Not Found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Put_PlayerBySquadNumber_ValidationError_Returns422UnprocessableEntity()
|
||||
{
|
||||
// Arrange — SquadNumber -1 fails GreaterThan(0)
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(23);
|
||||
request.SquadNumber = -1;
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status422UnprocessableEntity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Put_PlayerBySquadNumber_SquadNumberMismatch_Returns400BadRequest()
|
||||
{
|
||||
// Arrange — body squad number (99) differs from route (23); both are valid values
|
||||
// so validation passes and the mismatch guard in the controller fires instead
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(23);
|
||||
request.SquadNumber = 99;
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status400BadRequest);
|
||||
problem.Title.Should().Be("Bad Request");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* DELETE /players/squadNumber/{squadNumber:int}
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Delete_PlayerBySquadNumber_Existing_Returns204NoContent()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.DeleteAsync("/players/squadNumber/23");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Delete_PlayerBySquadNumber_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.DeleteAsync("/players/squadNumber/999");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
|
||||
problem!.Status.Should().Be(StatusCodes.Status404NotFound);
|
||||
problem.Title.Should().Be("Not Found");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* GET /health
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
public async Task Get_Health_Healthy_Returns200Ok()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/health");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Controllers;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit;
|
||||
|
||||
public class PlayerControllerTests : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public PlayerControllerTests()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* HTTP POST
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Post_Players_ValidationError_Returns422UnprocessableEntity()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(
|
||||
new ValidationResult(
|
||||
new List<ValidationFailure>
|
||||
{
|
||||
new("SquadNumber", "SquadNumber must be greater than 0."),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.PostAsync(request);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Never);
|
||||
service.Verify(service => service.CreateAsync(It.IsAny<PlayerRequestModel>()), Times.Never);
|
||||
validator.Verify(
|
||||
validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity);
|
||||
var problemDetails = httpResult
|
||||
.ProblemDetails.Should()
|
||||
.BeOfType<HttpValidationProblemDetails>()
|
||||
.Subject;
|
||||
problemDetails.Status.Should().Be(StatusCodes.Status422UnprocessableEntity);
|
||||
problemDetails
|
||||
.Errors.Should()
|
||||
.ContainKey("SquadNumber")
|
||||
.WhoseValue.Should()
|
||||
.Contain("SquadNumber must be greater than 0.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Post_Players_Existing_Returns409Conflict()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
var response = PlayerFakes.MakeResponseModelForCreate();
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(response);
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(new ValidationResult(new List<ValidationFailure> { }));
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.PostAsync(request);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
service.Verify(service => service.CreateAsync(It.IsAny<PlayerRequestModel>()), Times.Never);
|
||||
validator.Verify(
|
||||
validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
var httpResult = result.Should().BeOfType<Conflict<ProblemDetails>>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Post_Players_Nonexistent_Returns201Created()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
var response = PlayerFakes.MakeResponseModelForCreate();
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(null as PlayerResponseModel);
|
||||
service.Setup(service => service.CreateAsync(request)).ReturnsAsync(response);
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(new ValidationResult(new List<ValidationFailure> { }));
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object)
|
||||
{
|
||||
Url = PlayerMocks.SetupUrlHelperMock().Object,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await controller.PostAsync(request);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
service.Verify(service => service.CreateAsync(It.IsAny<PlayerRequestModel>()), Times.Once);
|
||||
validator.Verify(
|
||||
validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
var httpResult = result.Should().BeOfType<CreatedAtRoute<PlayerResponseModel>>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status201Created);
|
||||
httpResult.Value.Should().BeEquivalentTo(response);
|
||||
httpResult.RouteName.Should().Be("RetrieveBySquadNumber");
|
||||
httpResult.RouteValues.Should().NotBeNull().And.ContainKey("squadNumber");
|
||||
httpResult.RouteValues!["squadNumber"].Should().Be(response.Dorsal);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* HTTP GET
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Get_Players_Existing_ReturnsPlayers()
|
||||
{
|
||||
// Arrange
|
||||
var response = PlayerFakes.MakeResponseModelsForRetrieve();
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service.Setup(service => service.RetrieveAsync()).ReturnsAsync(response);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.GetAsync();
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveAsync(), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<Ok<List<PlayerResponseModel>>>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status200OK);
|
||||
httpResult.Value.Should().NotBeNull().And.BeOfType<List<PlayerResponseModel>>();
|
||||
httpResult.Value.Should().BeEquivalentTo(response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Get_Players_Empty_Returns200OkWithEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service.Setup(service => service.RetrieveAsync()).ReturnsAsync([]);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.GetAsync();
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveAsync(), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<Ok<List<PlayerResponseModel>>>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status200OK);
|
||||
httpResult.Value.Should().NotBeNull().And.BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Get_PlayerById_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveByIdAsync(id))
|
||||
.ReturnsAsync(null as PlayerResponseModel);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.GetByIdAsync(id);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveByIdAsync(It.IsAny<Guid>()), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Get_PlayerById_Existing_Returns200OK()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var response = PlayerFakes.MakeResponseModelForRetrieve(10);
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service.Setup(service => service.RetrieveByIdAsync(id)).ReturnsAsync(response);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.GetByIdAsync(id);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveByIdAsync(It.IsAny<Guid>()), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<Ok<PlayerResponseModel>>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status200OK);
|
||||
httpResult.Value.Should().NotBeNull().And.BeOfType<PlayerResponseModel>();
|
||||
httpResult.Value.Should().BeEquivalentTo(response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Get_PlayerBySquadNumber_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 999;
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(null as PlayerResponseModel);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.GetBySquadNumberAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Get_PlayerBySquadNumber_Existing_Returns200OK()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 10;
|
||||
var response = PlayerFakes.MakeResponseModelForRetrieve(squadNumber);
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(response);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.GetBySquadNumberAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<Ok<PlayerResponseModel>>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status200OK);
|
||||
httpResult.Value.Should().NotBeNull().And.BeOfType<PlayerResponseModel>();
|
||||
httpResult.Value.Should().BeEquivalentTo(response);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* HTTP PUT
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Put_PlayerBySquadNumber_ValidationError_Returns422UnprocessableEntity()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 20;
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber);
|
||||
request.SquadNumber = -999; // Invalid Squad Number
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(
|
||||
new ValidationResult(
|
||||
new List<ValidationFailure>
|
||||
{
|
||||
new("SquadNumber", "SquadNumber must be greater than 0."),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await controller.PutAsync(squadNumber, request);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Never);
|
||||
service.Verify(service => service.UpdateAsync(It.IsAny<PlayerRequestModel>()), Times.Never);
|
||||
validator.Verify(
|
||||
validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity);
|
||||
var problemDetails = httpResult
|
||||
.ProblemDetails.Should()
|
||||
.BeOfType<HttpValidationProblemDetails>()
|
||||
.Subject;
|
||||
problemDetails.Status.Should().Be(StatusCodes.Status422UnprocessableEntity);
|
||||
problemDetails
|
||||
.Errors.Should()
|
||||
.ContainKey("SquadNumber")
|
||||
.WhoseValue.Should()
|
||||
.Contain("SquadNumber must be greater than 0.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Put_PlayerBySquadNumber_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 999;
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(null as PlayerResponseModel);
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(new ValidationResult(new List<ValidationFailure> { }));
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.PutAsync(
|
||||
squadNumber,
|
||||
new PlayerRequestModel { SquadNumber = squadNumber }
|
||||
);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
service.Verify(service => service.UpdateAsync(It.IsAny<PlayerRequestModel>()), Times.Never);
|
||||
validator.Verify(
|
||||
validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
),
|
||||
Times.Once
|
||||
);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Put_PlayerBySquadNumber_SquadNumberMismatch_Returns400BadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 23;
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber);
|
||||
request.SquadNumber = 99; // Mismatched squad number
|
||||
var response = PlayerFakes.MakeResponseModelForUpdate(squadNumber);
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(response);
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(new ValidationResult(new List<ValidationFailure> { }));
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.PutAsync(squadNumber, request);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Never);
|
||||
service.Verify(service => service.UpdateAsync(It.IsAny<PlayerRequestModel>()), Times.Never);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Put_PlayerBySquadNumber_Existing_Returns204NoContent()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 23;
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber);
|
||||
var response = PlayerFakes.MakeResponseModelForUpdate(squadNumber);
|
||||
request.FirstName = "Emiliano";
|
||||
request.MiddleName = string.Empty;
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(response);
|
||||
service.Setup(service => service.UpdateAsync(request));
|
||||
validator
|
||||
.Setup(validator =>
|
||||
validator.ValidateAsync(
|
||||
It.IsAny<IValidationContext>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
)
|
||||
)
|
||||
.ReturnsAsync(new ValidationResult(new List<ValidationFailure> { }));
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.PutAsync(squadNumber, request);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
service.Verify(service => service.UpdateAsync(It.IsAny<PlayerRequestModel>()), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<NoContent>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status204NoContent);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* HTTP DELETE
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Delete_PlayerBySquadNumber_Unknown_Returns404NotFound()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 999;
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(null as PlayerResponseModel);
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.DeleteAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
service.Verify(service => service.DeleteAsync(It.IsAny<int>()), Times.Never);
|
||||
var httpResult = result.Should().BeOfType<ProblemHttpResult>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task Delete_PlayerBySquadNumber_Existing_Returns204NoContent()
|
||||
{
|
||||
// Arrange
|
||||
var response = PlayerFakes.MakeResponseModelForCreate();
|
||||
var squadNumber = response.Dorsal;
|
||||
var (service, logger, validator) = PlayerMocks.InitControllerMocks();
|
||||
service
|
||||
.Setup(service => service.RetrieveBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(response);
|
||||
service.Setup(service => service.DeleteAsync(squadNumber));
|
||||
|
||||
var controller = new PlayerController(service.Object, logger.Object, validator.Object);
|
||||
|
||||
// Act
|
||||
var result = await controller.DeleteAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny<int>()), Times.Once);
|
||||
service.Verify(service => service.DeleteAsync(It.IsAny<int>()), Times.Once);
|
||||
var httpResult = result.Should().BeOfType<NoContent>().Subject;
|
||||
httpResult.StatusCode.Should().Be(StatusCodes.Status204NoContent);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
using System.Diagnostics;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Services;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit;
|
||||
|
||||
public class PlayerServiceTests
|
||||
{
|
||||
/* -------------------------------------------------------------------------
|
||||
* Create
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task CreateAsync_WhenCalled_AddsPlayerAndRemovesCache()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
var response = PlayerFakes.MakeResponseModelForCreate();
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
mapper.Setup(mapper => mapper.Map<PlayerResponseModel>(request)).Returns(response);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
await service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
repository.Verify(repository => repository.AddAsync(It.IsAny<Player>()), Times.Once);
|
||||
memoryCache.Verify(cache => cache.Remove(It.IsAny<object>()), Times.Once);
|
||||
mapper.Verify(
|
||||
mapper => mapper.Map<PlayerResponseModel>(It.IsAny<PlayerRequestModel>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Retrieve
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task RetrieveAsync_CacheMiss_QueriesRepositoryAndCachesResult()
|
||||
{
|
||||
// Arrange
|
||||
var value = It.IsAny<object>();
|
||||
var players = PlayerFakes.MakeStarting11();
|
||||
var response = PlayerFakes.MakeResponseModelsForRetrieve();
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players);
|
||||
mapper.Setup(mapper => mapper.Map<List<PlayerResponseModel>>(players)).Returns(response);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await service.RetrieveAsync();
|
||||
|
||||
// Assert
|
||||
repository.Verify(repository => repository.GetAllAsync(), Times.Once);
|
||||
memoryCache.Verify(cache => cache.TryGetValue(It.IsAny<object>(), out value), Times.Once);
|
||||
memoryCache.Verify(cache => cache.CreateEntry(It.IsAny<object>()), Times.Once);
|
||||
mapper.Verify(
|
||||
mapper => mapper.Map<List<PlayerResponseModel>>(It.IsAny<List<Player>>()),
|
||||
Times.Once
|
||||
);
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task RetrieveAsync_CacheHit_ReturnsBeforeQueryingRepository()
|
||||
{
|
||||
// Arrange
|
||||
var value = It.IsAny<object>();
|
||||
var players = PlayerFakes.MakeStarting11();
|
||||
var response = PlayerFakes.MakeResponseModelsForRetrieve();
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players);
|
||||
mapper.Setup(mapper => mapper.Map<List<PlayerResponseModel>>(players)).Returns(response);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
var first = await ExecutionTimeAsync(() => service.RetrieveAsync());
|
||||
var second = await ExecutionTimeAsync(() => service.RetrieveAsync());
|
||||
|
||||
// Assert
|
||||
memoryCache.Verify(
|
||||
cache => cache.TryGetValue(It.IsAny<object>(), out value),
|
||||
Times.Exactly(2) // first + second
|
||||
);
|
||||
memoryCache.Verify(cache => cache.CreateEntry(It.IsAny<object>()), Times.Once); // first only
|
||||
repository.Verify(repository => repository.GetAllAsync(), Times.Once); // first only
|
||||
mapper.Verify(
|
||||
mapper => mapper.Map<List<PlayerResponseModel>>(It.IsAny<List<Player>>()),
|
||||
Times.Once // first only
|
||||
);
|
||||
second.Should().BeLessThan(first);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task RetrieveByIdAsync_PlayerNotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository.Setup(repository => repository.FindByIdAsync(id)).ReturnsAsync(null as Player);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await service.RetrieveByIdAsync(id);
|
||||
|
||||
// Assert
|
||||
repository.Verify(repository => repository.FindByIdAsync(It.IsAny<Guid>()), Times.Once);
|
||||
mapper.Verify(mapper => mapper.Map<PlayerResponseModel>(It.IsAny<Player>()), Times.Never);
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task RetrieveByIdAsync_PlayerFound_ReturnsMappedResponseModel()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var squadNumber = 10;
|
||||
var player = PlayerFakes.MakeFromStarting11(squadNumber);
|
||||
var response = PlayerFakes.MakeResponseModelForRetrieve(squadNumber);
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository.Setup(repository => repository.FindByIdAsync(id)).ReturnsAsync(player);
|
||||
mapper.Setup(mapper => mapper.Map<PlayerResponseModel>(player)).Returns(response);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await service.RetrieveByIdAsync(id);
|
||||
|
||||
// Assert
|
||||
repository.Verify(repository => repository.FindByIdAsync(It.IsAny<Guid>()), Times.Once);
|
||||
mapper.Verify(mapper => mapper.Map<PlayerResponseModel>(It.IsAny<Player>()), Times.Once);
|
||||
result.Should().BeOfType<PlayerResponseModel>();
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task RetrieveBySquadNumberAsync_PlayerNotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 999;
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(null as Player);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await service.RetrieveBySquadNumberAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
repository.Verify(
|
||||
repository => repository.FindBySquadNumberAsync(It.IsAny<int>()),
|
||||
Times.Once
|
||||
);
|
||||
mapper.Verify(mapper => mapper.Map<PlayerResponseModel>(It.IsAny<Player>()), Times.Never);
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task RetrieveBySquadNumberAsync_PlayerFound_ReturnsMappedResponseModel()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 10;
|
||||
var player = PlayerFakes.MakeFromStarting11(squadNumber);
|
||||
var response = PlayerFakes.MakeResponseModelForRetrieve(squadNumber);
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(player);
|
||||
mapper.Setup(mapper => mapper.Map<PlayerResponseModel>(player)).Returns(response);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await service.RetrieveBySquadNumberAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
repository.Verify(
|
||||
repository => repository.FindBySquadNumberAsync(It.IsAny<int>()),
|
||||
Times.Once
|
||||
);
|
||||
mapper.Verify(mapper => mapper.Map<PlayerResponseModel>(It.IsAny<Player>()), Times.Once);
|
||||
result.Should().BeOfType<PlayerResponseModel>();
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Update
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task UpdateAsync_PlayerFound_UpdatesRepositoryAndRemovesCache()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 23;
|
||||
var player = PlayerFakes.MakeFromStarting11(squadNumber);
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber);
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(player);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
await service.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
repository.Verify(
|
||||
repository => repository.FindBySquadNumberAsync(It.IsAny<int>()),
|
||||
Times.Once
|
||||
);
|
||||
repository.Verify(repository => repository.UpdateAsync(It.IsAny<Player>()), Times.Once);
|
||||
memoryCache.Verify(cache => cache.Remove(It.IsAny<object>()), Times.Once);
|
||||
mapper.Verify(
|
||||
mapper => mapper.Map(It.IsAny<PlayerRequestModel>(), It.IsAny<Player>()),
|
||||
Times.Once
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task UpdateAsync_PlayerNotFound_DoesNotUpdateRepository()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 999;
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.SquadNumber = squadNumber;
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(null as Player);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
await service.UpdateAsync(request);
|
||||
|
||||
// Assert
|
||||
repository.Verify(
|
||||
repository => repository.FindBySquadNumberAsync(It.IsAny<int>()),
|
||||
Times.Once
|
||||
);
|
||||
repository.Verify(repository => repository.UpdateAsync(It.IsAny<Player>()), Times.Never);
|
||||
memoryCache.Verify(cache => cache.Remove(It.IsAny<object>()), Times.Never);
|
||||
mapper.Verify(
|
||||
mapper => mapper.Map(It.IsAny<PlayerRequestModel>(), It.IsAny<Player>()),
|
||||
Times.Never
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Delete
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task DeleteAsync_PlayerFound_RemovesFromRepositoryAndRemovesCache()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 26;
|
||||
var player = PlayerFakes.MakeFromStarting11(squadNumber);
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(player);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
await service.DeleteAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
repository.Verify(
|
||||
repository => repository.FindBySquadNumberAsync(It.IsAny<int>()),
|
||||
Times.Once
|
||||
);
|
||||
repository.Verify(repository => repository.RemoveAsync(It.IsAny<Guid>()), Times.Once);
|
||||
memoryCache.Verify(cache => cache.Remove(It.IsAny<object>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task DeleteAsync_PlayerNotFound_DoesNotRemoveFromRepository()
|
||||
{
|
||||
// Arrange
|
||||
var squadNumber = 999;
|
||||
var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks();
|
||||
repository
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(squadNumber))
|
||||
.ReturnsAsync(null as Player);
|
||||
|
||||
var service = new PlayerService(
|
||||
repository.Object,
|
||||
logger.Object,
|
||||
memoryCache.Object,
|
||||
mapper.Object,
|
||||
environment.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
await service.DeleteAsync(squadNumber);
|
||||
|
||||
// Assert
|
||||
repository.Verify(
|
||||
repository => repository.FindBySquadNumberAsync(It.IsAny<int>()),
|
||||
Times.Once
|
||||
);
|
||||
repository.Verify(repository => repository.RemoveAsync(It.IsAny<Guid>()), Times.Never);
|
||||
memoryCache.Verify(cache => cache.Remove(It.IsAny<object>()), Times.Never);
|
||||
}
|
||||
|
||||
private static async Task<long> ExecutionTimeAsync(Func<Task> awaitable)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
|
||||
stopwatch.Start();
|
||||
await awaitable();
|
||||
stopwatch.Stop();
|
||||
|
||||
return stopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Validators;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using Moq;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit;
|
||||
|
||||
public class PlayerValidatorTests
|
||||
{
|
||||
private sealed class FakeTimeProvider(DateTimeOffset utcNow) : TimeProvider
|
||||
{
|
||||
public override DateTimeOffset GetUtcNow() => utcNow;
|
||||
}
|
||||
|
||||
private static PlayerRequestModelValidator CreateValidator(
|
||||
Mock<IPlayerRepository>? repositoryMock = null,
|
||||
TimeProvider? timeProvider = null
|
||||
)
|
||||
{
|
||||
var mock = repositoryMock ?? new Mock<IPlayerRepository>();
|
||||
return new PlayerRequestModelValidator(mock.Object, timeProvider);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Valid request
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_ValidRequest_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
var repositoryMock = new Mock<IPlayerRepository>();
|
||||
repositoryMock
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(null as Player);
|
||||
var validator = CreateValidator(repositoryMock);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* FirstName
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_FirstNameEmpty_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.FirstName = string.Empty;
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "FirstName");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* LastName
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_LastNameEmpty_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.LastName = string.Empty;
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "LastName");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* SquadNumber
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_SquadNumberNotGreaterThanZero_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.SquadNumber = 0;
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "SquadNumber");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_SquadNumberNegative_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.SquadNumber = -5;
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result
|
||||
.Errors.Should()
|
||||
.Contain(error =>
|
||||
error.PropertyName == "SquadNumber" && error.ErrorMessage.Contains("greater than 0")
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_SquadNumberNotUnique_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
var existingPlayer = PlayerFakes.MakeNew();
|
||||
var repositoryMock = new Mock<IPlayerRepository>();
|
||||
repositoryMock
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(existingPlayer);
|
||||
var validator = CreateValidator(repositoryMock);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result
|
||||
.Errors.Should()
|
||||
.Contain(error =>
|
||||
error.PropertyName == "SquadNumber" && error.ErrorMessage.Contains("unique")
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_SquadNumberBelongsToPlayerBeingUpdated_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
// Simulate a PUT request for an existing player: the squad number in the
|
||||
// body matches the one already in the database. The "Update" rule set must
|
||||
// not run BeUniqueSquadNumber, otherwise this valid request would be rejected.
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(10);
|
||||
var existingPlayer = PlayerFakes.MakeFromStarting11(10);
|
||||
var repositoryMock = new Mock<IPlayerRepository>();
|
||||
repositoryMock
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(existingPlayer);
|
||||
var validator = CreateValidator(repositoryMock);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Update")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_FirstNameEmptyInUpdateRuleSet_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForUpdate(10);
|
||||
request.FirstName = string.Empty;
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Update")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "FirstName");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* AbbrPosition
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_AbbrPositionEmpty_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.AbbrPosition = string.Empty;
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "AbbrPosition");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_AbbrPositionInvalid_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.AbbrPosition = "INVALID";
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "AbbrPosition");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* DateOfBirth
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_DateOfBirthNull_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.DateOfBirth = null;
|
||||
var repositoryMock = new Mock<IPlayerRepository>();
|
||||
repositoryMock
|
||||
.Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(null as Player);
|
||||
var validator = CreateValidator(repositoryMock);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_DateOfBirthInFuture_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.DateOfBirth = DateTime.UtcNow.AddYears(1);
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "DateOfBirth");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_DateOfBirthToday_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var fixedNow = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var timeProvider = new FakeTimeProvider(fixedNow);
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.DateOfBirth = fixedNow.Date; // same "today" the validator sees
|
||||
var validator = CreateValidator(timeProvider: timeProvider);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_DateOfBirthBeforeYear1900_ReturnsValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.DateOfBirth = new DateTime(1899, 12, 31, 0, 0, 0, DateTimeKind.Utc);
|
||||
var validator = CreateValidator();
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().Contain(error => error.PropertyName == "DateOfBirth");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Unit")]
|
||||
public async Task ValidateAsync_DateOfBirthOnJanuary1st1900_ReturnsNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var request = PlayerFakes.MakeRequestModelForCreate();
|
||||
request.DateOfBirth = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); // rule: >= 1900-01-01
|
||||
var repositoryMock = new Mock<IPlayerRepository>();
|
||||
repositoryMock
|
||||
.Setup(r => r.FindBySquadNumberAsync(request.SquadNumber))
|
||||
.ReturnsAsync(null as Player);
|
||||
var validator = CreateValidator(repositoryMock);
|
||||
|
||||
// Act
|
||||
var result = await validator.ValidateAsync(
|
||||
request,
|
||||
options => options.IncludeRuleSets("Create")
|
||||
);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global using Xunit;
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Data.Common;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Data;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Migrations;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// A Fake is a working implementation that's simpler than the real one.
|
||||
/// It usually has some "real" logic but is not suitable for production
|
||||
/// (e.g., an in‑memory database instead of a full SQL Server). Fakes are
|
||||
/// useful when you need behavior that's closer to reality but still want
|
||||
/// to avoid external dependencies.
|
||||
/// </summary>
|
||||
public static class DatabaseFakes
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an in-memory SQLite connection and DbContext options for testing.
|
||||
/// The connection remains open for the lifetime of the test.
|
||||
/// </summary>
|
||||
/// <returns>A tuple containing the SQLite connection and DbContext options.</returns>
|
||||
public static (DbConnection, DbContextOptions<PlayerDbContext>) CreateSqliteConnection()
|
||||
{
|
||||
var dbConnection = new SqliteConnection("Filename=:memory:");
|
||||
dbConnection.Open();
|
||||
|
||||
var dbContextOptions = new DbContextOptionsBuilder<PlayerDbContext>()
|
||||
.UseSqlite(dbConnection)
|
||||
.ReplaceService<IMigrationsAssembly, ProviderSpecificMigrationsAssembly>()
|
||||
.Options;
|
||||
|
||||
return (dbConnection, dbContextOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerDbContext instance with the specified options.
|
||||
/// </summary>
|
||||
/// <param name="dbContextOptions">The DbContext options to use.</param>
|
||||
/// <returns>A new PlayerDbContext instance.</returns>
|
||||
public static PlayerDbContext CreateDbContext(
|
||||
DbContextOptions<PlayerDbContext> dbContextOptions
|
||||
)
|
||||
{
|
||||
return new PlayerDbContext(dbContextOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies all pending EF Core migrations to the test database, including seed data.
|
||||
/// Extension method for PlayerDbContext.
|
||||
/// </summary>
|
||||
/// <param name="context">The PlayerDbContext instance.</param>
|
||||
public static async Task MigrateAsync(this PlayerDbContext context)
|
||||
{
|
||||
await context.Database.MigrateAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Utilities;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Test data factory for Player entities.
|
||||
/// Wraps production data from PlayerData with test-specific modifications (e.g., GUID assignment).
|
||||
/// A Fake is a working implementation that's simpler than the real one.
|
||||
/// It usually has some "real" logic but is not suitable for production
|
||||
/// (e.g., an in‑memory database instead of a full SQL Server). Fakes are
|
||||
/// useful when you need behavior that's closer to reality but still want
|
||||
/// to avoid external dependencies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Data-state vocabulary used across controller tests:
|
||||
/// <list type="bullet">
|
||||
/// <item><term>existing</term><description>Player is present in the database.</description></item>
|
||||
/// <item><term>nonexistent</term><description>Player is absent, valid shape for creation (POST scenarios).</description></item>
|
||||
/// <item><term>unknown</term><description>Valid ID format, absent from database (404-by-lookup scenarios).</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static class PlayerFakes
|
||||
{
|
||||
private static string? FormatBirth(DateTime? dateOfBirth) =>
|
||||
dateOfBirth.HasValue ? $"{dateOfBirth.Value:MMMM d, yyyy}" : null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the starting 11 players with generated GUIDs for in-memory testing.
|
||||
/// Reuses production player data from PlayerData.MakeStarting11().
|
||||
/// </summary>
|
||||
public static List<Player> MakeStarting11()
|
||||
{
|
||||
var players = PlayerData.MakeStarting11();
|
||||
|
||||
// Assign GUIDs for in-memory database testing
|
||||
foreach (var player in players)
|
||||
{
|
||||
player.Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a specific player from the starting 11 by squad number.
|
||||
/// Reuses production player data from PlayerData.MakeStarting11().
|
||||
/// </summary>
|
||||
public static Player MakeFromStarting11(int squadNumber)
|
||||
{
|
||||
var player =
|
||||
PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber)
|
||||
?? throw new ArgumentNullException(
|
||||
$"Player with Squad Number {squadNumber} not found."
|
||||
);
|
||||
|
||||
player.Id = Guid.NewGuid();
|
||||
return player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new player (substitute) for testing create operations.
|
||||
/// Reuses production player data from PlayerData.GetSubstitutes().
|
||||
/// </summary>
|
||||
public static Player MakeNew()
|
||||
{
|
||||
// Get Giovani Lo Celso (squad number 27) from substitutes
|
||||
var player =
|
||||
PlayerData.GetSubstitutes().SingleOrDefault(player => player.SquadNumber == 27)
|
||||
?? throw new InvalidOperationException(
|
||||
"Substitute player with squad number 27 not found."
|
||||
);
|
||||
|
||||
player.Id = Guid.NewGuid();
|
||||
return player;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Create
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerRequestModel for testing create operations.
|
||||
/// Uses data from a substitute player (Giovani Lo Celso).
|
||||
/// </summary>
|
||||
public static PlayerRequestModel MakeRequestModelForCreate()
|
||||
{
|
||||
var player = MakeNew();
|
||||
|
||||
return new PlayerRequestModel()
|
||||
{
|
||||
FirstName = player.FirstName,
|
||||
MiddleName = player.MiddleName,
|
||||
LastName = player.LastName,
|
||||
DateOfBirth = player.DateOfBirth,
|
||||
SquadNumber = player.SquadNumber,
|
||||
AbbrPosition = player.AbbrPosition,
|
||||
Team = player.Team,
|
||||
League = player.League,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerResponseModel for testing create operation responses.
|
||||
/// Uses data from a substitute player (Giovani Lo Celso).
|
||||
/// </summary>
|
||||
public static PlayerResponseModel MakeResponseModelForCreate()
|
||||
{
|
||||
var player = MakeNew();
|
||||
|
||||
return new PlayerResponseModel
|
||||
{
|
||||
FullName =
|
||||
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
|
||||
Birth = FormatBirth(player.DateOfBirth),
|
||||
Dorsal = player.SquadNumber,
|
||||
Position = player.Position,
|
||||
Club = player.Team,
|
||||
League = player.League,
|
||||
Starting11 = player.Starting11 ? "Yes" : "No",
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Retrieve
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerRequestModel for testing retrieve operations.
|
||||
/// Uses data from the starting 11 based on the specified squad number.
|
||||
/// </summary>
|
||||
/// <param name="squadNumber">The squad number of the player to retrieve.</param>
|
||||
public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber)
|
||||
{
|
||||
var player =
|
||||
PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber)
|
||||
?? throw new ArgumentNullException(
|
||||
$"Player with Squad Number {squadNumber} not found."
|
||||
);
|
||||
|
||||
return new PlayerRequestModel
|
||||
{
|
||||
FirstName = player.FirstName,
|
||||
MiddleName = player.MiddleName,
|
||||
LastName = player.LastName,
|
||||
DateOfBirth = player.DateOfBirth,
|
||||
SquadNumber = player.SquadNumber,
|
||||
AbbrPosition = player.AbbrPosition,
|
||||
Team = player.Team,
|
||||
League = player.League,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerResponseModel for testing retrieve operation responses.
|
||||
/// Uses data from the starting 11 based on the specified squad number.
|
||||
/// </summary>
|
||||
/// <param name="squadNumber">The squad number of the player to retrieve.</param>
|
||||
public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
|
||||
{
|
||||
var player =
|
||||
PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber)
|
||||
?? throw new ArgumentNullException(
|
||||
$"Player with Squad Number {squadNumber} not found."
|
||||
);
|
||||
|
||||
return new PlayerResponseModel
|
||||
{
|
||||
FullName =
|
||||
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
|
||||
Birth = FormatBirth(player.DateOfBirth),
|
||||
Dorsal = player.SquadNumber,
|
||||
Position = player.Position,
|
||||
Club = player.Team,
|
||||
League = player.League,
|
||||
Starting11 = player.Starting11 ? "Yes" : "No",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a list of PlayerResponseModel for testing retrieve all operations.
|
||||
/// Uses all players from the starting 11.
|
||||
/// </summary>
|
||||
public static List<PlayerResponseModel> MakeResponseModelsForRetrieve() =>
|
||||
[
|
||||
.. PlayerData
|
||||
.MakeStarting11()
|
||||
.Select(player =>
|
||||
{
|
||||
player.Id = Guid.NewGuid();
|
||||
return new PlayerResponseModel
|
||||
{
|
||||
FullName =
|
||||
$"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
|
||||
Birth = FormatBirth(player.DateOfBirth),
|
||||
Dorsal = player.SquadNumber,
|
||||
Position = player.Position,
|
||||
Club = player.Team,
|
||||
League = player.League,
|
||||
Starting11 = player.Starting11 ? "Yes" : "No",
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Update
|
||||
* ---------------------------------------------------------------------- */
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerRequestModel for testing update operations.
|
||||
/// Delegates to MakeRequestModelForRetrieve.
|
||||
/// </summary>
|
||||
/// <param name="squadNumber">The squad number of the player to update.</param>
|
||||
public static PlayerRequestModel MakeRequestModelForUpdate(int squadNumber)
|
||||
{
|
||||
return MakeRequestModelForRetrieve(squadNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PlayerResponseModel for testing update operation responses.
|
||||
/// Delegates to MakeResponseModelForRetrieve.
|
||||
/// </summary>
|
||||
/// <param name="squadNumber">The squad number of the player to update.</param>
|
||||
public static PlayerResponseModel MakeResponseModelForUpdate(int squadNumber)
|
||||
{
|
||||
return MakeResponseModelForRetrieve(squadNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using AutoMapper;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Controllers;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Models;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Repositories;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Services;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// A Mock is a dynamic test double that not only provides responses but
|
||||
/// also records interactions. Mocks are pre‑programmed with expectations
|
||||
/// (like “this method should be called once with these parameters”) and can
|
||||
/// later verify that the expected calls occurred. Frameworks such as Moq
|
||||
/// make it easy to set up and verify these interactions.
|
||||
/// </summary>
|
||||
public static class PlayerMocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes mocks for PlayerController dependencies.
|
||||
/// </summary>
|
||||
/// <returns>A tuple containing mocked service, logger, and validator.</returns>
|
||||
public static (
|
||||
Mock<IPlayerService> service,
|
||||
Mock<ILogger<PlayerController>> logger,
|
||||
Mock<IValidator<PlayerRequestModel>> validator
|
||||
) InitControllerMocks()
|
||||
{
|
||||
var service = new Mock<IPlayerService>();
|
||||
var logger = new Mock<ILogger<PlayerController>>();
|
||||
var validator = new Mock<IValidator<PlayerRequestModel>>();
|
||||
|
||||
return (service, logger, validator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock IUrlHelper configured to return any string for Action calls.
|
||||
/// </summary>
|
||||
/// <returns>A configured Mock of IUrlHelper.</returns>
|
||||
public static Mock<IUrlHelper> SetupUrlHelperMock()
|
||||
{
|
||||
var mock = new Mock<IUrlHelper>();
|
||||
mock.Setup(url => url.Action(It.IsAny<UrlActionContext>())).Returns(It.IsAny<string>());
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes mocks for PlayerService dependencies.
|
||||
/// </summary>
|
||||
/// <param name="cacheValue">Optional cache value to return from memory cache.</param>
|
||||
/// <param name="environmentName">The environment name (defaults to "Development").</param>
|
||||
/// <returns>A tuple containing mocked repository, logger, cache, mapper, and environment.</returns>
|
||||
public static (
|
||||
Mock<IPlayerRepository> repository,
|
||||
Mock<ILogger<PlayerService>> logger,
|
||||
Mock<IMemoryCache> memoryCache,
|
||||
Mock<IMapper> mapper,
|
||||
Mock<IHostEnvironment> environment
|
||||
) InitServiceMocks(object? cacheValue = null, string environmentName = "Development")
|
||||
{
|
||||
var repository = new Mock<IPlayerRepository>();
|
||||
var logger = new Mock<ILogger<PlayerService>>();
|
||||
var memoryCache = SetupMemoryCacheMock(cacheValue ?? It.IsAny<object>());
|
||||
var mapper = new Mock<IMapper>();
|
||||
var environment = new Mock<IHostEnvironment>();
|
||||
environment.Setup(env => env.EnvironmentName).Returns(environmentName);
|
||||
return (repository, logger, memoryCache, mapper, environment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock IMemoryCache with configurable behavior.
|
||||
/// First TryGetValue call returns false, subsequent calls return true with the specified value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to return from cache after the first call.</param>
|
||||
/// <returns>A configured Mock of IMemoryCache.</returns>
|
||||
public static Mock<IMemoryCache> SetupMemoryCacheMock(object? value)
|
||||
{
|
||||
var cachedValue = false;
|
||||
var mock = new Mock<IMemoryCache>();
|
||||
|
||||
mock.Setup(cache => cache.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny!))
|
||||
.Callback(
|
||||
new TryGetValueDelegate(
|
||||
(object _, out object? output) =>
|
||||
{
|
||||
output = cachedValue ? value : null;
|
||||
}
|
||||
)
|
||||
)
|
||||
.Returns(() =>
|
||||
{
|
||||
bool hasValue = cachedValue;
|
||||
cachedValue = true; // Subsequent invocations will return true
|
||||
return hasValue;
|
||||
});
|
||||
|
||||
mock.Setup(cache => cache.CreateEntry(It.IsAny<object>()))
|
||||
.Returns(Mock.Of<ICacheEntry>);
|
||||
mock.Setup(cache => cache.Remove(It.IsAny<object>()));
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private delegate void TryGetValueDelegate(object key, out object? value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Data.Common;
|
||||
using Dotnet.Samples.AspNetCore.WebApi.Data;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// A Stub provides pre‑configured, hard‑coded responses to method calls.
|
||||
/// Its purpose is simply to supply data to the system under test without
|
||||
/// any behavior verification. For example, a Stub for a repository might
|
||||
/// always return a specific user regardless of the input.
|
||||
/// </summary>
|
||||
public static class PlayerStubs
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a ModelStateDictionary with a single validation error.
|
||||
/// Used for testing validation failure scenarios.
|
||||
/// </summary>
|
||||
/// <param name="key">The property name or key for the error.</param>
|
||||
/// <param name="errorMessage">The error message.</param>
|
||||
/// <returns>A ModelStateDictionary containing the specified error.</returns>
|
||||
public static ModelStateDictionary CreateModelError(string key, string errorMessage)
|
||||
{
|
||||
var modelStateDictionary = new ModelStateDictionary();
|
||||
modelStateDictionary.AddModelError(key, errorMessage);
|
||||
return modelStateDictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// A test authentication handler that automatically authenticates every request,
|
||||
/// bypassing the real authentication scheme. Used in integration tests to exercise
|
||||
/// endpoints protected with [Authorize] without requiring a real token.
|
||||
/// </summary>
|
||||
internal sealed class TestAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder
|
||||
) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
public const string SchemeName = "Test";
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new[] { new Claim(ClaimTypes.Name, "TestUser") };
|
||||
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, SchemeName);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"coverlet.collector": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.1, )",
|
||||
"resolved": "10.0.1",
|
||||
"contentHash": "27jXSV/0DbVqF5jDrAxuQFZ9oaz6gmG03p8ttxAFk+X0M4woFYj7MoWDLCna5EGLb0CE6OE7X6ZH3Wt5smTtaA=="
|
||||
},
|
||||
"FluentAssertions": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.10.0, )",
|
||||
"resolved": "8.10.0",
|
||||
"contentHash": "yGvv+xp0gHFgGwhQcGoDdKIrhLhPi3zYYWWLLHPMctr+G6PWD7QH8L4wqoa5WRZquPbfSV/cP1MraI7ppAHrWw=="
|
||||
},
|
||||
"Microsoft.AspNetCore.Mvc.Testing": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.9, )",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "Mt+5CtYz+xmog1S1TJt2owVKU8YquZtNy9bmO+kfrrtjEDZPzCw1qZ7o97PLpIEpt3yy4F5YdAUh9nKPm0CX5Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.TestHost": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyModel": "10.0.9",
|
||||
"Microsoft.Extensions.Hosting": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.Test.Sdk": {
|
||||
"type": "Direct",
|
||||
"requested": "[18.7.0, )",
|
||||
"resolved": "18.7.0",
|
||||
"contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "18.7.0",
|
||||
"Microsoft.TestPlatform.TestHost": "18.7.0"
|
||||
}
|
||||
},
|
||||
"Moq": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.20.72, )",
|
||||
"resolved": "4.20.72",
|
||||
"contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==",
|
||||
"dependencies": {
|
||||
"Castle.Core": "5.1.1"
|
||||
}
|
||||
},
|
||||
"xunit": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.9.3, )",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.18.0",
|
||||
"xunit.assert": "2.9.3",
|
||||
"xunit.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.runner.visualstudio": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.5, )",
|
||||
"resolved": "3.1.5",
|
||||
"contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA=="
|
||||
},
|
||||
"AutoMapper": {
|
||||
"type": "Transitive",
|
||||
"resolved": "16.1.1",
|
||||
"contentHash": "VNEky8JA15ci+oIDRGHITOGOpV4dILsf8pnn24QhDl2urtqgJ2IXiS/V2EtGU17P/+f6OeFQPJETaZXV9QOIZg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.0",
|
||||
"Microsoft.Extensions.Options": "10.0.0",
|
||||
"Microsoft.IdentityModel.JsonWebTokens": "8.14.0"
|
||||
}
|
||||
},
|
||||
"Castle.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.1.1",
|
||||
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "6.0.0"
|
||||
}
|
||||
},
|
||||
"FluentValidation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.1",
|
||||
"contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw=="
|
||||
},
|
||||
"FluentValidation.DependencyInjectionExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.1",
|
||||
"contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==",
|
||||
"dependencies": {
|
||||
"FluentValidation": "12.1.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.OpenApi": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "1ihb8FO9cGgEK1/m3CTtT/SfnynwmiZib0W2pcDVj3KSWk/Sca4VOXEtaptKQc582zpFrzTFiwkGRCglt6H+WQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.OpenApi": "2.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.TestHost": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "mR4y30XFsVbn7EwV3Ic5/KMBO5QGwukTJE2ztGmpAST5ACX+Z+9r+Y6D1eibJojsOIW5KHpM4myo9/aRALJOyg=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.7.0",
|
||||
"contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "iZrONyMKPjxfVZnUktqO30QjzNwAGH+AxM61s8lKQnVhgbQ3bn0hiXI129ZmVicEbIcwljyy2OVsIYUR51ZHKQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "tu85SRzOT021V7EQlViCiAE7TqldVn469Y6lt5TEn/+XC4/MeNCHgMRSxqYuWqvF4zAQZUhCmtNEZuM3ss4LeA==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.9",
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.9",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "GRMaiPkqYna/gCsyDffYDWmefGPC3hDrdMw+2rrGcQwhs6uZOsaMQXMJnoXQ35tx9SkBV2ieRRU9N/jLOO6BZw=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "aiEFB+C5EsZGqxvMPazE07hbWsp4iPaufJpanGt5O+lrwv7mJLrqma5haVIgFAPCyhQkmk75XSCEubT1zUjxtA=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "dEoyYKgiaZHHgOFm1WMWm1sFEsEuhPWufX4L9PekKtqd/RaIcPjkCjvbrVvJtApErb5wPSJhYvnTlxhH+p9h2g==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "10.0.9",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "HH9/nnRF/YmRrc3hUlgXjMBYKH5kFmd5UWC81l9U0ySQhwHTcgvDPSewB8DyQHzFJzNGgG7VFK6ynC6+XQz9WA==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.9",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyModel": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "3bPEmACAaPJJSw+m5XwTSi3yZnVtaifa4d8gLsNMzW0Qu28jS5kADSfgJRBlq49RJ1K098VCzEDRJwM8gE6f2w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "10.0.9",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "10.0.9",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyModel": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.ApiDescription.Server": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg=="
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "5fGxcw2vuYp8s0wio9H1ECiuk4iKSdTIlNuigdLIrkhg+5XAwgFVDB/5Ots3pfN/QhABLYXutA79JFtnUKDSHA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "G9mregdatGWMCQWeCw012LDeJVP7G/XIxH8Ddbjc8bD1//dA+8VVQdcRE9jI1moyoJxSSZhHITUnNQ8FUDl5+Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "woZsWLhOQsASuxbmgiZJqiGUBNo3IjRdXC92xt8rRokza+P6/nIsnzq7sm9Or6ZYcRl2kL1ufj8HVzp1QlPTXw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "qGhRPd3VxfLV9UqatVOiD9mAeUbj2KiMwGFYC5uXlzExiZQoe4X/hdmzGIU7BQjNLTqCnnbTHVyBglG3668/HA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "Tp/+LPb70RyjjtLg9m5C959eP4KrUpJHThZfAegZVpsfmGvzfuNkuYbI/ft+LvXhMSyUcAeOPaN6rzTccwnZAg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.CommandLine": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "8D4HaqxWdm5M/nuhQffjPoR1ekhlpyKTXjFMAT5KlP0dvxkJe5JLAP6MAsuUEUxKWG09Bi5aAUaYMFKrMqWHqA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "JhKySWIL8+N4yFt4HPm1rGKCHooze+MBdTdpXc0bd/PGm31TrSUi2m0Nek1y441Wlv/RE6VH0W/DCv2xnmy8FA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "NgLB9cYnIb0/djSDcnqo4GIGGWooxGmr/gCUe3/CRXcKqLizOFui8MyW4EVkTB/KNJL+oXdMXnD6ZRm3Y+qkrQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Physical": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "LiFKJgc9jZEW+7RhcSfsvCwoikt1lDdOqOn+whZC5zVHyg/gExftHl2QPtmfiHsEdDNg+Y+BDr6835tOfj8Y7A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.UserSecrets": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "ockJRreRW/HbGwoyHzYOxMucFBimvAZ8lKNwQLMHrS6mwkDUaCJMWzzeE+Rm9vgFlv2o/xqk8fm+FpqrDCnkTA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Json": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Physical": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "NijozhERJDIaJ4k5TSMy1jOi0cSC2HfkvRD/Sl+kGSSKgVbFnF4GxgtMN/MrzHB8D1JxIrD4xSer9Blh9v3axQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "SCDTQ6HubnRvTUjR7dgMKHZvNoCb03t44ttHL8trlFTGgfDteWn/0nRdOxDhcI+lTWhKgd/flCVJEtAOPhSLNg=="
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "NLXI3PbTe39q6/sgs7JYhmfPf7bMzReUoAJ0q9Po6yhfM+0anZa7PrEva4W2SdiLWGyB9eKZS9THGt2BP40xJg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "86RgyFsmVslW4Nu28IXgt8tLglynGQrwjk/xhGZaTe8j6YIeR1Ywoc42hSHsBSl920CQdfqq2dBohZiGm3AkUA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "Oxn4vqDk+EwceTMpZxVm7L/UZEAM1qIQlNP1+7tBZckD+P4SKrm/5X4gMTPCTdpnau/xY8Sb4/0d6onomSg4ZA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Physical": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "zm8WVod4swgprGrkxkuSILlbXqdDRqF+3y6U0I7jlmj4PMyKN6d8pzXZHUn5lr/gZVULzk/+FeTYlTupt6akpg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.FileSystemGlobbing": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileSystemGlobbing": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "mvRf9qOH/LslWIee/h+lsElnoUyKotEwoPL31soqScmO/eoxObaTCLCdx2DdqPdRi9LnB+7qKZ49jfyrLZuc+w=="
|
||||
},
|
||||
"Microsoft.Extensions.Hosting": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "HTgnvmK0ubesUFO16pLC+i9+RS8lEGd6TmDouuy75FsAgIFrSwUVhYCqG2IENzBJwgxGc/6Rsulfsvd9ZG/XkA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Binder": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.CommandLine": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Json": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyInjection": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Diagnostics": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Physical": "10.0.9",
|
||||
"Microsoft.Extensions.Hosting.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Console": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Debug": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.EventLog": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.EventSource": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Hosting.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "Xd/2F+uWblTiUp+ssaDZN2ea4vmnHmW6PXugmqBHumyhqVkyeh6RJ3S2Zo/F+1bXIL/KuGqe2pKv6UiGOc1KeQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "N7Gm9SjugYjmmnhwbBKC9DFqGqjfJvh6YfOJgtwh0AW0Xpok3dIVors1ik050XmUxKAgAc7nNngDIJyFb06K2g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "9S/DFt4cohlMPpzIxjG6kk0L8MuN2vDm9pbMCulxtJzzk82oJHVLBd8vuQxaPskaYQwKqmFmbannf5eoChgjYg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "bUth5ip7YsZMXWZS42IRTI0zDrPEqdE+xnsmcL0Pk784grWKApDvc5UoMi2tP2qYJ5ylFzeVDuDu08sFATq1bg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Binder": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "WyZEG/O8jKqBOBF6/M6IJqiEyWFBUv6PDyzNoXDA0mBZwKtkuf7GiZ/0/8eU8OpLKKQL0O95oPOY1szrWIKofQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Configuration": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Debug": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "r/A0ahpXmZH/8ltPjrFFWp12BIizK9cCVJXPcHyOad8e4eIX7P/geW+uBYdczkeCAaMebT4jEU7snOm4GnmKfA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "goAl30/WwmdnWDPRwATaDPIK0iuDBnQSMTH2XYGVB1SwReg7hglhvDNjjpNhT25US3GF4I5q6BhTNs6nFYzEfg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9",
|
||||
"System.Diagnostics.EventLog": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.EventSource": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "tHynPVHbTicuaDpS2JVTxX0qA5VTg15CXgVKTwWvvudb5BvW5aVew8MMyek6LrDGAom7UbON1jf1T5GhpTilFA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Logging": "10.0.9",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "hyNdX4c2UwkRkzb9byw0H2DQkRzwBM3mzY2sCM9egwzTyg8dvQJmp5noQHGEaaCORQrNK3DD2gREBsc2DlXS4A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "Y4E24zffF/aPS0igNvY6ZzAQfbxd6AYdC9L4brnH+uK0yYYHIR6FeGVQVVjAOo8wub1EQDl2B90lCcpqoTF7Yw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Configuration.Binder": "10.0.9",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9",
|
||||
"Microsoft.Extensions.Options": "10.0.9",
|
||||
"Microsoft.Extensions.Primitives": "10.0.9"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "fmEbAUFsaIKirgLt/lYhuFRBwhcSJN31jjHgCdbQxJiWOum6EdLjkbgGuukSP9z/a+9LibaxII/kF+GwOXgC4g=="
|
||||
},
|
||||
"Microsoft.IdentityModel.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.14.0",
|
||||
"contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ=="
|
||||
},
|
||||
"Microsoft.IdentityModel.JsonWebTokens": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.14.0",
|
||||
"contentHash": "4jOpiA4THdtpLyMdAb24dtj7+6GmvhOhxf5XHLYWmPKF8ApEnApal1UnJsKO4HxUWRXDA6C4WQVfYyqsRhpNpQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Tokens": "8.14.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.IdentityModel.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.14.0",
|
||||
"contentHash": "eqqnemdW38CKZEHS6diA50BV94QICozDZEvSrsvN3SJXUFwVB9gy+/oz76gldP7nZliA16IglXjXTCTdmU/Ejg==",
|
||||
"dependencies": {
|
||||
"Microsoft.IdentityModel.Abstractions": "8.14.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.IdentityModel.Tokens": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.14.0",
|
||||
"contentHash": "lKIZiBiGd36k02TCdMHp1KlNWisyIvQxcYJvIkz7P4gSQ9zi8dgh6S5Grj8NNG7HWYIPfQymGyoZ6JB5d1Lo1g==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
||||
"Microsoft.IdentityModel.Logging": "8.14.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.OpenApi": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.7.5",
|
||||
"contentHash": "0FA67RSnRM4tcBKqiqVu/HPdZ9+QOKbmeRjxRUGTCjPU4C0bmUhd97Dso7Yild5P7nOV6GxJ2xrK0Kv/O9xp0w=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.7.0",
|
||||
"contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA=="
|
||||
},
|
||||
"Microsoft.TestPlatform.TestHost": {
|
||||
"type": "Transitive",
|
||||
"resolved": "18.7.0",
|
||||
"contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==",
|
||||
"dependencies": {
|
||||
"Microsoft.TestPlatform.ObjectModel": "18.7.0",
|
||||
"Newtonsoft.Json": "13.0.3"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.3",
|
||||
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
|
||||
},
|
||||
"Npgsql": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.0"
|
||||
}
|
||||
},
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.2",
|
||||
"contentHash": "PsNYgPOSW41Xx19gin7y4EdZAPteWr9Cb01XkdObxOsPzi+mgBupBEN7J7+erXFsROPOILM7MlIoO9QzL8+LGQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
|
||||
"Npgsql": "10.0.3"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
"contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ=="
|
||||
},
|
||||
"Serilog.AspNetCore": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.3.0",
|
||||
"Serilog.Extensions.Hosting": "10.0.0",
|
||||
"Serilog.Formatting.Compact": "3.0.0",
|
||||
"Serilog.Settings.Configuration": "10.0.0",
|
||||
"Serilog.Sinks.Console": "6.1.1",
|
||||
"Serilog.Sinks.Debug": "3.0.0",
|
||||
"Serilog.Sinks.File": "7.0.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Extensions.Hosting": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
|
||||
"Microsoft.Extensions.Hosting.Abstractions": "10.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.0",
|
||||
"Serilog": "4.3.0",
|
||||
"Serilog.Extensions.Logging": "10.0.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.0",
|
||||
"contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging": "10.0.0",
|
||||
"Serilog": "4.2.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Formatting.Compact": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.0.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Settings.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.1",
|
||||
"contentHash": "ayFE7h66mqMqzwfPrzDCMbWU27FdNC2bkCG+jnkeHFZTBRh+yWdr4aa/2WuX7c8RmqxGPMW2UqoJ3fw9hK3QhA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Binder": "10.0.0",
|
||||
"Microsoft.Extensions.DependencyModel": "10.0.0",
|
||||
"Serilog": "4.3.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Sinks.Console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.1.1",
|
||||
"contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.0.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Sinks.Debug": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.0.0"
|
||||
}
|
||||
},
|
||||
"Serilog.Sinks.File": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==",
|
||||
"dependencies": {
|
||||
"Serilog": "4.2.0"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Swashbuckle.AspNetCore": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.2.3",
|
||||
"contentHash": "8KNh1RWvofdU6DVLyBs4Z/OpUMnmf8oNvJQc0QxpwySRbi42bwLfdVMMrXZWANg5U5KQGQq1xW6r/hlcqw99tQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.ApiDescription.Server": "10.0.0",
|
||||
"Swashbuckle.AspNetCore.Swagger": "10.2.3",
|
||||
"Swashbuckle.AspNetCore.SwaggerGen": "10.2.3",
|
||||
"Swashbuckle.AspNetCore.SwaggerUI": "10.2.3"
|
||||
}
|
||||
},
|
||||
"Swashbuckle.AspNetCore.Swagger": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.2.3",
|
||||
"contentHash": "1jUUs3WQnrS0FUtaZPLSy1yYMEwS1zlvDmvQ2/eldPHUANX0LJSLVZecCMgSMdeGiRqeaRrIXLtSz++TCiTMww==",
|
||||
"dependencies": {
|
||||
"Microsoft.OpenApi": "2.7.5"
|
||||
}
|
||||
},
|
||||
"Swashbuckle.AspNetCore.SwaggerGen": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.2.3",
|
||||
"contentHash": "y7t4coDRAeFYChmvlMRiH2OjbiRrm9AVIDgt17fQfs3x9PVAI5PiwWYOhg+4F13R4Q36WDc9lqfoOnNa3tNbGg==",
|
||||
"dependencies": {
|
||||
"Swashbuckle.AspNetCore.Swagger": "10.2.3"
|
||||
}
|
||||
},
|
||||
"Swashbuckle.AspNetCore.SwaggerUI": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.2.3",
|
||||
"contentHash": "nthWONRs/FJ4yyG206g1cC52WEG8EqrjuMWjGdR+5XG7lbjFto6NqcI9EMICgVFom/UivIjUVwI76ZHbHwTPfQ=="
|
||||
},
|
||||
"System.Diagnostics.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "s2PcxHK4IYQ6gmD3VSBkym9tWGkFisKjcjWBdl7a+n4Yy66ae4beJ1ZdjDp060SSll4W3Rt4H2LW87dWckv+QQ=="
|
||||
},
|
||||
"xunit.abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.18.0",
|
||||
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
|
||||
},
|
||||
"xunit.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
|
||||
},
|
||||
"xunit.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.3]",
|
||||
"xunit.extensibility.execution": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
|
||||
"dependencies": {
|
||||
"xunit.abstractions": "2.0.3"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.execution": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.3",
|
||||
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"dotnet.samples.aspnetcore.webapi": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"AutoMapper": "[16.1.1, 17.0.0)",
|
||||
"FluentValidation": "[12.1.1, )",
|
||||
"FluentValidation.DependencyInjectionExtensions": "[12.1.1, )",
|
||||
"Microsoft.AspNetCore.OpenApi": "[10.0.9, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[10.0.9, )",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.2, )",
|
||||
"Serilog.AspNetCore": "[10.0.0, )",
|
||||
"Serilog.Settings.Configuration": "[10.0.1, )",
|
||||
"Serilog.Sinks.Console": "[6.1.1, )",
|
||||
"Serilog.Sinks.File": "[7.0.0, )",
|
||||
"Swashbuckle.AspNetCore": "[10.2.3, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user