mirror of
https://github.com/meysamhadeli/booking-microservices.git
synced 2026-04-10 17:59:38 +08:00
feat(flight):add initial Unit.Test project
This commit is contained in:
parent
628257ba6c
commit
660cac9ee5
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" />
|
||||
@ -21,6 +21,17 @@
|
||||
<Protobuf Include="GrpcServer\Protos\flight.proto" GrpcServices="Both" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Unit.Test</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Integration.Test</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\..\building-blocks\BuildingBlocks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using global::Flight.Aircrafts.Features.CreatingAircraft.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateAircraftCommand : AutoFaker<CreateAircraft>
|
||||
{
|
||||
public FakeCreateAircraftCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.ManufacturingYear, _ => 2000);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using global::Flight.Airports.Features.CreatingAirport.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateAirportCommand : AutoFaker<CreateAirport>
|
||||
{
|
||||
public FakeCreateAirportCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using AutoBogus;
|
||||
using Flight.Flights.Enums;
|
||||
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using System.Linq;
|
||||
using global::Flight.Data.Seed;
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
using MassTransit;
|
||||
|
||||
public sealed class FakeCreateFlightCommand : AutoFaker<CreateFlight>
|
||||
{
|
||||
public FakeCreateFlightCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.FlightNumber, r => r.Random.Number(1000, 2000).ToString());
|
||||
RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id);
|
||||
RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id);
|
||||
RuleFor(r => r.Status, _ => FlightStatus.Flying);
|
||||
RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using System.Linq;
|
||||
using AutoBogus;
|
||||
using global::Flight.Data.Seed;
|
||||
using global::Flight.Flights.Enums;
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
using MassTransit;
|
||||
|
||||
public sealed class FakeCreateFlightMongoCommand : AutoFaker<CreateFlightMongo>
|
||||
{
|
||||
public FakeCreateFlightMongoCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.FlightNumber, r => "12FF");
|
||||
RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id);
|
||||
RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id);
|
||||
RuleFor(r => r.Status, _ => FlightStatus.Flying);
|
||||
RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id);
|
||||
RuleFor(r => r.IsDeleted, _ => false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
using AutoBogus;
|
||||
using Flight.Seats.Enums;
|
||||
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using System;
|
||||
using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateSeatCommand : AutoFaker<CreateSeat>
|
||||
{
|
||||
public FakeCreateSeatCommand(Guid flightId)
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.FlightId, _ => flightId);
|
||||
RuleFor(r => r.Class, _ => SeatClass.Economy);
|
||||
RuleFor(r => r.Type, _ => SeatType.Middle);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using System;
|
||||
using AutoBogus;
|
||||
using global::Flight.Seats.Enums;
|
||||
using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateSeatMongoCommand : AutoFaker<CreateSeatMongo>
|
||||
{
|
||||
public FakeCreateSeatMongoCommand(Guid flightId)
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.FlightId, _ => flightId);
|
||||
RuleFor(r => r.Class, _ => SeatClass.Economy);
|
||||
RuleFor(r => r.Type, _ => SeatType.Middle);
|
||||
RuleFor(r => r.IsDeleted, _ => false);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Integration.Test.Fakes;
|
||||
|
||||
using global::Flight.Flights.Features.UpdatingFlight.V1;
|
||||
|
||||
public class FakeUpdateFlightCommand : AutoFaker<UpdateFlight>
|
||||
{
|
||||
public FakeUpdateFlightCommand(global::Flight.Flights.Models.Flight flight)
|
||||
{
|
||||
RuleFor(r => r.Id, _ => flight.Id);
|
||||
RuleFor(r => r.DepartureAirportId, _ => flight.DepartureAirportId);
|
||||
RuleFor(r => r.ArriveAirportId, _ => flight.ArriveAirportId);
|
||||
RuleFor(r => r.AircraftId, _ => flight.AircraftId);
|
||||
RuleFor(r => r.FlightNumber, r => r.Random.Number(1000, 2000).ToString());
|
||||
RuleFor(r => r.Price, _ => 800);
|
||||
RuleFor(r => r.Status, _ => flight.Status);
|
||||
RuleFor(r => r.ArriveDate, _ => flight.ArriveDate);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using Api;
|
||||
using BuildingBlocks.TestBase;
|
||||
using Flight.Data;
|
||||
using Xunit;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
[Collection(IntegrationTestCollection.Name)]
|
||||
public class FlightIntegrationTestBase : TestBase<Program, FlightDbContext, FlightReadDbContext>
|
||||
{
|
||||
public FlightIntegrationTestBase(TestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(integrationTestFixture)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public class IntegrationTestCollection : ICollectionFixture<TestFixture<Program, FlightDbContext, FlightReadDbContext>>
|
||||
{
|
||||
public const string Name = "Flight Integration Test";
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
using BuildingBlocks.EFCore;
|
||||
using Flight.Aircrafts.Models;
|
||||
using Flight.Airports.Models;
|
||||
using Flight.Data;
|
||||
using Flight.Data.Seed;
|
||||
using Flight.Flights.Models;
|
||||
using Flight.Seats.Models;
|
||||
using MapsterMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Driver.Linq;
|
||||
|
||||
namespace Integration.Test;
|
||||
|
||||
public class FlightTestDataSeeder(
|
||||
FlightDbContext flightDbContext,
|
||||
FlightReadDbContext flightReadDbContext,
|
||||
IMapper mapper
|
||||
) : ITestDataSeeder
|
||||
{
|
||||
public async Task SeedAllAsync()
|
||||
{
|
||||
await SeedAirportAsync();
|
||||
await SeedAircraftAsync();
|
||||
await SeedFlightAsync();
|
||||
await SeedSeatAsync();
|
||||
}
|
||||
|
||||
private async Task SeedAirportAsync()
|
||||
{
|
||||
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Airports))
|
||||
{
|
||||
await flightDbContext.Airports.AddRangeAsync(InitialData.Airports);
|
||||
await flightDbContext.SaveChangesAsync();
|
||||
|
||||
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Airport.AsQueryable()))
|
||||
{
|
||||
await flightReadDbContext.Airport.InsertManyAsync(mapper.Map<List<AirportReadModel>>(InitialData.Airports));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeedAircraftAsync()
|
||||
{
|
||||
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Aircraft))
|
||||
{
|
||||
await flightDbContext.Aircraft.AddRangeAsync(InitialData.Aircrafts);
|
||||
await flightDbContext.SaveChangesAsync();
|
||||
|
||||
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Aircraft.AsQueryable()))
|
||||
{
|
||||
await flightReadDbContext.Aircraft.InsertManyAsync(mapper.Map<List<AircraftReadModel>>(InitialData.Aircrafts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task SeedSeatAsync()
|
||||
{
|
||||
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Seats))
|
||||
{
|
||||
await flightDbContext.Seats.AddRangeAsync(InitialData.Seats);
|
||||
await flightDbContext.SaveChangesAsync();
|
||||
|
||||
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Seat.AsQueryable()))
|
||||
{
|
||||
await flightReadDbContext.Seat.InsertManyAsync(mapper.Map<List<SeatReadModel>>(InitialData.Seats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeedFlightAsync()
|
||||
{
|
||||
if (!await EntityFrameworkQueryableExtensions.AnyAsync(flightDbContext.Flights))
|
||||
{
|
||||
await flightDbContext.Flights.AddRangeAsync(InitialData.Flights);
|
||||
await flightDbContext.SaveChangesAsync();
|
||||
|
||||
if (!await MongoQueryable.AnyAsync(flightReadDbContext.Flight.AsQueryable()))
|
||||
{
|
||||
await flightReadDbContext.Flight.InsertManyAsync(mapper.Map<List<FlightReadModel>>(InitialData.Flights));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\Api\src\Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,43 @@
|
||||
//using System.Threading.Tasks;
|
||||
//using BuildingBlocks.TestBase;
|
||||
//using Flight;
|
||||
//using Flight.Data;
|
||||
//using FluentAssertions;
|
||||
//using Integration.Test.Fakes;
|
||||
//using Xunit;
|
||||
|
||||
//namespace Integration.Test.Seat.Features;
|
||||
|
||||
//using Api;
|
||||
//using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
//using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
|
||||
//public class GetAvailableSeatsTests : FlightIntegrationTestBase
|
||||
//{
|
||||
// public GetAvailableSeatsTests(
|
||||
// TestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFactory) : base(integrationTestFactory)
|
||||
// {
|
||||
// }
|
||||
|
||||
// [Fact]
|
||||
// public async Task should_return_available_seats_from_grpc_service()
|
||||
// {
|
||||
// // Arrange
|
||||
// var flightCommand = new FakeCreateFlightMongoCommand().Generate();
|
||||
|
||||
// await Fixture.SendAsync(flightCommand);
|
||||
|
||||
// var seatCommand = new FakeCreateSeatMongoCommand(flightCommand.Id).Generate();
|
||||
|
||||
// await Fixture.SendAsync(seatCommand);
|
||||
|
||||
// var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(Fixture.Channel);
|
||||
|
||||
// // Act
|
||||
// var response = await flightGrpcClient.GetAvailableSeatsAsync(new GetAvailableSeatsRequest { FlightId = flightCommand.Id.ToString() });
|
||||
|
||||
// // Assert
|
||||
// response?.Should().NotBeNull();
|
||||
// response?.SeatDtos?.Count.Should().BeGreaterOrEqualTo(1);
|
||||
// }
|
||||
//}
|
||||
@ -0,0 +1,43 @@
|
||||
//using System.Threading.Tasks;
|
||||
//using Api;
|
||||
//using BuildingBlocks.TestBase;
|
||||
//using Flight;
|
||||
//using Flight.Data;
|
||||
//using FluentAssertions;
|
||||
//using Integration.Test.Fakes;
|
||||
//using Xunit;
|
||||
|
||||
//namespace Integration.Test.Seat.Features;
|
||||
//public class ReserveSeatTests : FlightIntegrationTestBase
|
||||
//{
|
||||
// public ReserveSeatTests(
|
||||
// TestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFactory) : base(integrationTestFactory)
|
||||
// {
|
||||
// }
|
||||
|
||||
// [Fact]
|
||||
// public async Task should_return_valid_reserve_seat_from_grpc_service()
|
||||
// {
|
||||
// // Arrange
|
||||
// var flightCommand = new FakeCreateFlightCommand().Generate();
|
||||
|
||||
// await Fixture.SendAsync(flightCommand);
|
||||
|
||||
// var seatCommand = new FakeCreateSeatCommand(flightCommand.Id).Generate();
|
||||
|
||||
// await Fixture.SendAsync(seatCommand);
|
||||
|
||||
// var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(Fixture.Channel);
|
||||
|
||||
// // Act
|
||||
// var response = await flightGrpcClient.ReserveSeatAsync(new ReserveSeatRequest()
|
||||
// {
|
||||
// FlightId = seatCommand.FlightId.ToString(),
|
||||
// SeatNumber = seatCommand.SeatNumber
|
||||
// });
|
||||
|
||||
// // Assert
|
||||
// response?.Should().NotBeNull();
|
||||
// response?.Id.Should().Be(seatCommand?.Id.ToString());
|
||||
// }
|
||||
//}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"parallelizeAssembly": false,
|
||||
"parallelizeTestCollections": false
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
using FluentAssertions;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Aircraft.Features.CreateAircraftTests;
|
||||
|
||||
using global::Flight.Aircrafts.Features.CreatingAircraft.V1;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateAircraftCommandHandlerTests
|
||||
{
|
||||
private readonly UnitTestFixture _fixture;
|
||||
private readonly CreateAircraftHandler _handler;
|
||||
|
||||
public Task<CreateAircraftResult> Act(CreateAircraft command, CancellationToken cancellationToken) =>
|
||||
_handler.Handle(command, cancellationToken);
|
||||
|
||||
public CreateAircraftCommandHandlerTests(UnitTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CreateAircraftHandler(_fixture.DbContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_valid_command_should_create_new_aircraft_and_return_currect_aircraft_dto()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeCreateAircraftCommand().Generate();
|
||||
|
||||
// Act
|
||||
var response = await Act(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var entity = await _fixture.DbContext.Aircraft.FindAsync(response?.Id);
|
||||
|
||||
entity?.Should().NotBeNull();
|
||||
response?.Id.Should().Be(entity.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_null_command_should_throw_argument_exception()
|
||||
{
|
||||
// Arrange
|
||||
CreateAircraft command = null;
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => { await Act(command, CancellationToken.None); };
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
using FluentAssertions;
|
||||
using FluentValidation.TestHelper;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Aircraft.Features.CreateAircraftTests;
|
||||
|
||||
using global::Flight.Aircrafts.Features.CreatingAircraft.V1;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateAircraftCommandValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void is_valid_should_be_false_when_have_invalid_parameter()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeValidateCreateAircraftCommand().Generate();
|
||||
var validator = new CreateAircraftValidator();
|
||||
|
||||
// Act
|
||||
var result = validator.TestValidate(command);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.ShouldHaveValidationErrorFor(x => x.Model);
|
||||
result.ShouldHaveValidationErrorFor(x => x.ManufacturingYear);
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
using FluentAssertions;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Airport.Features.CreateAirportTests;
|
||||
|
||||
using global::Flight.Airports.Features.CreatingAirport.V1;
|
||||
using global::Flight.Airports.ValueObjects;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateAirportCommandHandlerTests
|
||||
{
|
||||
private readonly UnitTestFixture _fixture;
|
||||
private readonly CreateAirportHandler _handler;
|
||||
|
||||
|
||||
public CreateAirportCommandHandlerTests(UnitTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CreateAirportHandler(_fixture.DbContext);
|
||||
}
|
||||
|
||||
public Task<CreateAirportResult> Act(CreateAirport command, CancellationToken cancellationToken) =>
|
||||
_handler.Handle(command, cancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_valid_command_should_create_new_airport_and_return_currect_airport_dto()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeCreateAirportCommand().Generate();
|
||||
|
||||
// Act
|
||||
var response = await Act(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var entity = await _fixture.DbContext.Airports.FindAsync(AirportId.Of(response.Id));
|
||||
|
||||
entity?.Should().NotBeNull();
|
||||
response?.Id.Should().Be(entity.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_null_command_should_throw_argument_exception()
|
||||
{
|
||||
// Arrange
|
||||
CreateAirport command = null;
|
||||
|
||||
// Act
|
||||
var act = async () => { await Act(command, CancellationToken.None); };
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
using FluentAssertions;
|
||||
using FluentValidation.TestHelper;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Airport.Features.CreateAirportTests;
|
||||
|
||||
using global::Flight.Airports.Features.CreatingAirport.V1;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateAirportCommandValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void is_valid_should_be_false_when_have_invalid_parameter()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeValidateCreateAirportCommand().Generate();
|
||||
var validator = new CreateAirportValidator();
|
||||
|
||||
// Act
|
||||
var result = validator.TestValidate(command);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.ShouldHaveValidationErrorFor(x => x.Code);
|
||||
result.ShouldHaveValidationErrorFor(x => x.Address);
|
||||
result.ShouldHaveValidationErrorFor(x => x.Name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
using Flight.Data;
|
||||
using Flight.Flights.Enums;
|
||||
using Flight.Seats.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Unit.Test.Common;
|
||||
|
||||
using global::Flight.Aircrafts.ValueObjects;
|
||||
using global::Flight.Airports.ValueObjects;
|
||||
using global::Flight.Flights.ValueObjects;
|
||||
using global::Flight.Seats.ValueObjects;
|
||||
using MassTransit;
|
||||
using AirportName = global::Flight.Airports.ValueObjects.Name;
|
||||
using Name = global::Flight.Aircrafts.ValueObjects.Name;
|
||||
|
||||
public static class DbContextFactory
|
||||
{
|
||||
private static readonly Guid _airportId1 = NewId.NextGuid();
|
||||
private static readonly Guid _airportId2 = NewId.NextGuid();
|
||||
private static readonly Guid _aircraft1 = NewId.NextGuid();
|
||||
private static readonly Guid _aircraft2 = NewId.NextGuid();
|
||||
private static readonly Guid _aircraft3 = NewId.NextGuid();
|
||||
private static readonly Guid _flightId1 = NewId.NextGuid();
|
||||
|
||||
public static FlightDbContext Create()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<FlightDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
|
||||
|
||||
var context = new FlightDbContext(options, currentUserProvider: null, null);
|
||||
|
||||
// Seed our data
|
||||
FlightDataSeeder(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static void FlightDataSeeder(FlightDbContext context)
|
||||
{
|
||||
var airports = new List<global::Flight.Airports.Models.Airport>
|
||||
{
|
||||
global::Flight.Airports.Models.Airport.Create(AirportId.Of(_airportId1), AirportName.Of("Lisbon International Airport"), Address.Of("LIS"),
|
||||
Code.Of("12988")),
|
||||
global::Flight.Airports.Models.Airport.Create(AirportId.Of(_airportId2), AirportName.Of("Sao Paulo International Airport"), Address.Of("BRZ"),
|
||||
Code.Of("11200"))
|
||||
};
|
||||
|
||||
context.Airports.AddRange(airports);
|
||||
|
||||
var aircrafts = new List<global::Flight.Aircrafts.Models.Aircraft>
|
||||
{
|
||||
global::Flight.Aircrafts.Models.Aircraft.Create(AircraftId.Of(_aircraft1), Name.Of("Boeing 737"), Model.Of("B737"), ManufacturingYear.Of(2005)),
|
||||
global::Flight.Aircrafts.Models.Aircraft.Create(AircraftId.Of(_aircraft2), Name.Of("Airbus 300"), Model.Of("A300"), ManufacturingYear.Of(2000)),
|
||||
global::Flight.Aircrafts.Models.Aircraft.Create(AircraftId.Of(_aircraft3), Name.Of("Airbus 320"), Model.Of("A320"), ManufacturingYear.Of(2003))
|
||||
};
|
||||
|
||||
context.Aircraft.AddRange(aircrafts);
|
||||
|
||||
var flights = new List<global::Flight.Flights.Models.Flight>
|
||||
{
|
||||
global::Flight.Flights.Models.Flight.Create(FlightId.Of(_flightId1), FlightNumber.Of( "BD467"), AircraftId.Of(_aircraft1), AirportId.Of( _airportId1),
|
||||
DepartureDate.Of( new DateTime(2022, 1, 31, 12, 0, 0)),
|
||||
ArriveDate.Of( new DateTime(2022, 1, 31, 14, 0, 0)),
|
||||
AirportId.Of( _airportId2), DurationMinutes.Of(120m),
|
||||
FlightDate.Of( new DateTime(2022, 1, 31)), FlightStatus.Completed,
|
||||
Price.Of(8000))
|
||||
};
|
||||
context.Flights.AddRange(flights);
|
||||
|
||||
var seats = new List<global::Flight.Seats.Models.Seat>
|
||||
{
|
||||
global::Flight.Seats.Models.Seat.Create(SeatId.Of( NewId.NextGuid()), SeatNumber.Of("12A"), SeatType.Window, SeatClass.Economy,
|
||||
FlightId.Of(_flightId1)),
|
||||
global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12B"), SeatType.Window, SeatClass.Economy,
|
||||
FlightId.Of(_flightId1)),
|
||||
global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12C"), SeatType.Middle, SeatClass.Economy,
|
||||
FlightId.Of(_flightId1)),
|
||||
global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12D"), SeatType.Middle, SeatClass.Economy,
|
||||
FlightId.Of(_flightId1)),
|
||||
global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12E"), SeatType.Aisle, SeatClass.Economy,
|
||||
FlightId.Of(_flightId1)),
|
||||
global::Flight.Seats.Models.Seat.Create(SeatId.Of(NewId.NextGuid()), SeatNumber.Of("12F"), SeatType.Aisle, SeatClass.Economy,
|
||||
FlightId.Of(_flightId1))
|
||||
};
|
||||
|
||||
context.Seats.AddRange(seats);
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public static void Destroy(FlightDbContext context)
|
||||
{
|
||||
context.Database.EnsureDeleted();
|
||||
context.Dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using Flight;
|
||||
using Mapster;
|
||||
using MapsterMapper;
|
||||
|
||||
namespace Unit.Test.Common
|
||||
{
|
||||
public static class MapperFactory
|
||||
{
|
||||
public static IMapper Create()
|
||||
{
|
||||
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
|
||||
typeAdapterConfig.Scan(typeof(FlightRoot).Assembly);
|
||||
IMapper instance = new Mapper(typeAdapterConfig);
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
using Flight.Data;
|
||||
using MapsterMapper;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Common
|
||||
{
|
||||
[CollectionDefinition(nameof(UnitTestFixture))]
|
||||
public class FixtureCollection : ICollectionFixture<UnitTestFixture> { }
|
||||
|
||||
public class UnitTestFixture : IDisposable
|
||||
{
|
||||
public UnitTestFixture()
|
||||
{
|
||||
Mapper = MapperFactory.Create();
|
||||
DbContext = DbContextFactory.Create();
|
||||
}
|
||||
|
||||
public IMapper Mapper { get; }
|
||||
public FlightDbContext DbContext { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DbContextFactory.Destroy(DbContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Aircrafts.Features.CreatingAircraft.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateAircraftCommand : AutoFaker<CreateAircraft>
|
||||
{
|
||||
public FakeCreateAircraftCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.ManufacturingYear, _ => 2000);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Airports.Features.CreatingAirport.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateAirportCommand : AutoFaker<CreateAirport>
|
||||
{
|
||||
public FakeCreateAirportCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using System.Linq;
|
||||
using global::Flight.Data.Seed;
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
using MassTransit;
|
||||
|
||||
public sealed class FakeCreateFlightCommand : AutoFaker<CreateFlight>
|
||||
{
|
||||
public FakeCreateFlightCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.FlightNumber, r => r.Random.Number(1000, 2000).ToString());
|
||||
RuleFor(r => r.DepartureAirportId, _ => InitialData.Airports.First().Id);
|
||||
RuleFor(r => r.ArriveAirportId, _ => InitialData.Airports.Last().Id);
|
||||
RuleFor(r => r.AircraftId, _ => InitialData.Aircrafts.First().Id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
using AutoBogus;
|
||||
using Flight.Seats.Enums;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using System.Linq;
|
||||
using global::Flight.Data.Seed;
|
||||
using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
using MassTransit;
|
||||
|
||||
public class FakeCreateSeatCommand : AutoFaker<CreateSeat>
|
||||
{
|
||||
public FakeCreateSeatCommand()
|
||||
{
|
||||
RuleFor(r => r.Id, _ => NewId.NextGuid());
|
||||
RuleFor(r => r.FlightId, _ => InitialData.Flights.First().Id);
|
||||
RuleFor(r => r.SeatNumber, _ => "F99");
|
||||
RuleFor(r => r.Type, _ => SeatType.Window);
|
||||
RuleFor(r => r.Class, _ => SeatClass.Economy);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Aircrafts.ValueObjects;
|
||||
using global::Flight.Airports.ValueObjects;
|
||||
using global::Flight.Flights.ValueObjects;
|
||||
|
||||
public static class FakeFlightCreate
|
||||
{
|
||||
public static global::Flight.Flights.Models.Flight Generate()
|
||||
{
|
||||
var command = new FakeCreateFlightCommand().Generate();
|
||||
|
||||
return global::Flight.Flights.Models.Flight.Create(FlightId.Of(command.Id), FlightNumber.Of(command.FlightNumber),
|
||||
AircraftId.Of(command.AircraftId), AirportId.Of(command.DepartureAirportId), DepartureDate.Of(command.DepartureDate),
|
||||
ArriveDate.Of(command.ArriveDate), AirportId.Of(command.ArriveAirportId), DurationMinutes.Of(command.DurationMinutes),
|
||||
FlightDate.Of(command.FlightDate), command.Status, Price.Of(command.Price));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Flights.Models;
|
||||
using global::Flight.Flights.ValueObjects;
|
||||
|
||||
public static class FakeFlightUpdate
|
||||
{
|
||||
public static void Generate(Flight flight)
|
||||
{
|
||||
flight.Update(flight.Id, flight.FlightNumber, flight.AircraftId, flight.DepartureAirportId, flight.DepartureDate,
|
||||
flight.ArriveDate, flight.ArriveAirportId, flight.DurationMinutes, flight.FlightDate, flight.Status, Price.Of(1000), flight.IsDeleted);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Aircrafts.Features.CreatingAircraft.V1;
|
||||
|
||||
public class FakeValidateCreateAircraftCommand : AutoFaker<CreateAircraft>
|
||||
{
|
||||
public FakeValidateCreateAircraftCommand()
|
||||
{
|
||||
RuleFor(r => r.ManufacturingYear, _ => 0);
|
||||
RuleFor(r => r.Name, _ => null);
|
||||
RuleFor(r => r.Model, _ => null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using AutoBogus;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Airports.Features.CreatingAirport.V1;
|
||||
|
||||
public class FakeValidateCreateAirportCommand : AutoFaker<CreateAirport>
|
||||
{
|
||||
public FakeValidateCreateAirportCommand()
|
||||
{
|
||||
RuleFor(r => r.Code, _ => null);
|
||||
RuleFor(r => r.Name, _ => null);
|
||||
RuleFor(r => r.Address, _ => null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using AutoBogus;
|
||||
using Flight.Flights.Enums;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
|
||||
public class FakeValidateCreateFlightCommand : AutoFaker<CreateFlight>
|
||||
{
|
||||
public FakeValidateCreateFlightCommand()
|
||||
{
|
||||
RuleFor(r => r.Price, _ => -10);
|
||||
RuleFor(r => r.Status, _ => (FlightStatus)10);
|
||||
RuleFor(r => r.AircraftId, _ => Guid.Empty);
|
||||
RuleFor(r => r.DepartureAirportId, _ => Guid.Empty);
|
||||
RuleFor(r => r.ArriveAirportId, _ => Guid.Empty);
|
||||
RuleFor(r => r.DurationMinutes, _ => 0);
|
||||
RuleFor(r => r.FlightDate, _ => new DateTime());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using AutoBogus;
|
||||
using Flight.Seats.Enums;
|
||||
|
||||
namespace Unit.Test.Fakes;
|
||||
|
||||
using System;
|
||||
using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
|
||||
public class FakeValidateCreateSeatCommand : AutoFaker<CreateSeat>
|
||||
{
|
||||
public FakeValidateCreateSeatCommand()
|
||||
{
|
||||
RuleFor(r => r.SeatNumber, _ => null);
|
||||
RuleFor(r => r.FlightId, _ => Guid.Empty);
|
||||
RuleFor(r => r.Class, _ => (SeatClass)10);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
namespace Unit.Test.Flight.Features.Domains
|
||||
{
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateFlightTests
|
||||
{
|
||||
[Fact]
|
||||
public void can_create_valid_flight()
|
||||
{
|
||||
// Arrange + Act
|
||||
var fakeFlight = FakeFlightCreate.Generate();
|
||||
|
||||
// Assert
|
||||
fakeFlight.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void queue_domain_event_on_create()
|
||||
{
|
||||
// Arrange + Act
|
||||
var fakeFlight = FakeFlightCreate.Generate();
|
||||
|
||||
// Assert
|
||||
fakeFlight.DomainEvents.Count.Should().Be(1);
|
||||
fakeFlight.DomainEvents.FirstOrDefault().Should().BeOfType(typeof(FlightCreatedDomainEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
namespace Unit.Test.Flight.Features.Domains;
|
||||
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using global::Flight.Flights.Features.UpdatingFlight.V1;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class UpdateFlightTests
|
||||
{
|
||||
[Fact]
|
||||
public void can_update_valid_flight()
|
||||
{
|
||||
// Arrange
|
||||
var fakeFlight = FakeFlightCreate.Generate();
|
||||
|
||||
// Act
|
||||
FakeFlightUpdate.Generate(fakeFlight);
|
||||
|
||||
// Assert
|
||||
fakeFlight.Price.Value.Should().Be(1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void queue_domain_event_on_update()
|
||||
{
|
||||
// Arrange
|
||||
var fakeFlight = FakeFlightCreate.Generate();
|
||||
fakeFlight.ClearDomainEvents();
|
||||
|
||||
// Act
|
||||
FakeFlightUpdate.Generate(fakeFlight);
|
||||
|
||||
// Assert
|
||||
fakeFlight.DomainEvents.Count.Should().Be(1);
|
||||
fakeFlight.DomainEvents.FirstOrDefault().Should().BeOfType(typeof(FlightUpdatedDomainEvent));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
namespace Unit.Test.Flight.Features.Handlers.CreateFlight;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
using global::Flight.Flights.ValueObjects;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateFlightCommandHandlerTests
|
||||
{
|
||||
private readonly UnitTestFixture _fixture;
|
||||
private readonly CreateFlightHandler _handler;
|
||||
|
||||
public Task<CreateFlightResult> Act(CreateFlight command, CancellationToken cancellationToken) =>
|
||||
_handler.Handle(command, cancellationToken);
|
||||
|
||||
public CreateFlightCommandHandlerTests(UnitTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CreateFlightHandler(fixture.DbContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_valid_command_should_create_new_flight_and_return_currect_flight_dto()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeCreateFlightCommand().Generate();
|
||||
|
||||
// Act
|
||||
var response = await Act(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var entity = await _fixture.DbContext.Flights.FindAsync(FlightId.Of(response.Id));
|
||||
|
||||
entity?.Should().NotBeNull();
|
||||
response?.Id.Should().Be(entity.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_null_command_should_throw_argument_exception()
|
||||
{
|
||||
// Arrange
|
||||
CreateFlight command = null;
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => { await Act(command, CancellationToken.None); };
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
namespace Unit.Test.Flight.Features.Handlers.CreateFlight;
|
||||
|
||||
using FluentAssertions;
|
||||
using FluentValidation.TestHelper;
|
||||
using global::Flight.Flights.Features.CreatingFlight.V1;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateFlightCommandValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void is_valid_should_be_false_when_have_invalid_parameter()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeValidateCreateFlightCommand().Generate();
|
||||
var validator = new CreateFlightValidator();
|
||||
|
||||
// Act
|
||||
var result = validator.TestValidate(command);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.ShouldHaveValidationErrorFor(x => x.Price);
|
||||
result.ShouldHaveValidationErrorFor(x => x.Status);
|
||||
result.ShouldHaveValidationErrorFor(x => x.AircraftId);
|
||||
result.ShouldHaveValidationErrorFor(x => x.DepartureAirportId);
|
||||
result.ShouldHaveValidationErrorFor(x => x.ArriveAirportId);
|
||||
result.ShouldHaveValidationErrorFor(x => x.DurationMinutes);
|
||||
result.ShouldHaveValidationErrorFor(x => x.FlightDate);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using Flight.Flights.Dtos;
|
||||
using MapsterMapper;
|
||||
using Unit.Test.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Flight;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class FlightMappingTests
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public FlightMappingTests(UnitTestFixture fixture)
|
||||
{
|
||||
_mapper = fixture.Mapper;
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> Data
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
// these types will instantiate with reflection in the future
|
||||
typeof(global::Flight.Flights.Models.FlightReadModel), typeof(FlightDto)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Data))]
|
||||
public void should_support_mapping_from_source_to_destination(Type source, Type destination,
|
||||
params object[] parameters)
|
||||
{
|
||||
var instance = Activator.CreateInstance(source, parameters);
|
||||
|
||||
_mapper.Map(instance, source, destination);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
using FluentAssertions;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Seat.Features;
|
||||
|
||||
using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
using global::Flight.Seats.ValueObjects;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateSeatCommandHandlerTests
|
||||
{
|
||||
private readonly UnitTestFixture _fixture;
|
||||
private readonly CreateSeatCommandHandler _handler;
|
||||
|
||||
|
||||
public CreateSeatCommandHandlerTests(UnitTestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_handler = new CreateSeatCommandHandler(_fixture.DbContext);
|
||||
}
|
||||
|
||||
public Task<CreateSeatResult> Act(CreateSeat command, CancellationToken cancellationToken)
|
||||
{
|
||||
return _handler.Handle(command, cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_valid_command_should_create_new_seat_and_return_currect_seat_dto()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeCreateSeatCommand().Generate();
|
||||
|
||||
// Act
|
||||
var response = await Act(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var entity = await _fixture.DbContext.Seats.FindAsync(SeatId.Of(response.Id));
|
||||
|
||||
entity?.Should().NotBeNull();
|
||||
response?.Id.Should().Be(entity.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task handler_with_null_command_should_throw_argument_exception()
|
||||
{
|
||||
// Arrange
|
||||
CreateSeat command = null;
|
||||
|
||||
// Act
|
||||
var act = async () => { await Act(command, CancellationToken.None); };
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
using FluentAssertions;
|
||||
using FluentValidation.TestHelper;
|
||||
using Unit.Test.Common;
|
||||
using Unit.Test.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Seat.Features;
|
||||
|
||||
using global::Flight.Seats.Features.CreatingSeat.V1;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class CreateSeatCommandValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void is_valid_should_be_false_when_have_invalid_parameter()
|
||||
{
|
||||
// Arrange
|
||||
var command = new FakeValidateCreateSeatCommand().Generate();
|
||||
var validator = new CreateSeatValidator();
|
||||
|
||||
// Act
|
||||
var result = validator.TestValidate(command);
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.ShouldHaveValidationErrorFor(x => x.SeatNumber);
|
||||
result.ShouldHaveValidationErrorFor(x => x.FlightId);
|
||||
result.ShouldHaveValidationErrorFor(x => x.Class);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using Flight.Seats.Dtos;
|
||||
using MapsterMapper;
|
||||
using Unit.Test.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace Unit.Test.Seat;
|
||||
|
||||
[Collection(nameof(UnitTestFixture))]
|
||||
public class SeatMappingTests
|
||||
{
|
||||
private readonly UnitTestFixture _fixture;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public SeatMappingTests(UnitTestFixture fixture)
|
||||
{
|
||||
_mapper = fixture.Mapper;
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> Data
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
// these types will instantiate with reflection in the future
|
||||
typeof(global::Flight.Seats.Models.SeatReadModel), typeof(SeatDto)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Data))]
|
||||
public void should_support_mapping_from_source_to_destination(Type source, Type destination, params object[] parameters)
|
||||
{
|
||||
var instance = Activator.CreateInstance(source, parameters);
|
||||
|
||||
_mapper.Map(instance, source, destination);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Flight.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"parallelizeAssembly": false,
|
||||
"parallelizeTestCollections": false
|
||||
}
|
||||
@ -103,62 +103,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{EAAC4A89-D71
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "1-monolith-architecture-style\src\Api\src\Api.csproj", "{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Flight", "Flight", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unit.Test", "2-modular-monolith-architecture-style\src\Modules\Flight\tests\Unit.Test\Unit.Test.csproj", "{E7B7E65D-DB14-494C-A748-EF90666FB0B1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "2-modular-monolith-architecture-style\src\Modules\Flight\tests\Integration.Test\Integration.Test.csproj", "{AE7E4AE8-4A5C-44AE-B1FC-2A04824EE29B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{57366225-A26A-4715-A690-DA641B0C0450} = {33F5E6F6-FACB-4D34-8E48-B80E0497D239}
|
||||
{39BAB0A0-3C16-49B1-B817-9EC5C600BF5E} = {33F5E6F6-FACB-4D34-8E48-B80E0497D239}
|
||||
{570274DD-E84A-4F22-9079-D60B59EC9ED2} = {F1BEC66C-B321-45D8-95C1-8DCD4743275D}
|
||||
{AD2FB7C1-8641-47E9-B62D-B3A2D74147D8} = {F1BEC66C-B321-45D8-95C1-8DCD4743275D}
|
||||
{DBAE70CC-011A-4997-9612-58AFAFF73291} = {96913068-5EFE-4D13-8B00-924AFED16439}
|
||||
{FB529C7A-F884-42D1-BE41-2A6B7CC4101B} = {96913068-5EFE-4D13-8B00-924AFED16439}
|
||||
{5A2A9F1C-245A-4978-93A0-E00120FEE765} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{D0A6E1E7-DFB6-4940-A254-F2E24897C4AC} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{F77CA77C-49CE-49B5-BE5A-FCCF6C63D65A} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{A1786514-35EB-4AFA-87E1-2FB89D748C02} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{A5E49049-0DFF-4D87-9188-2B0ACBC0D59B} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{6538BDF3-A741-46E9-8988-C859ABB2FBB2} = {A5E49049-0DFF-4D87-9188-2B0ACBC0D59B}
|
||||
{B851799B-A328-4E40-9095-C56C11A6235E} = {D0A6E1E7-DFB6-4940-A254-F2E24897C4AC}
|
||||
{674C0974-11C4-4BF7-B2DF-8ED753919224} = {D0A6E1E7-DFB6-4940-A254-F2E24897C4AC}
|
||||
{FB8AA20B-0D31-4241-A126-07992BCF7E2A} = {5A2A9F1C-245A-4978-93A0-E00120FEE765}
|
||||
{0EB6B262-197D-450C-A56E-634D2D428FCB} = {5A2A9F1C-245A-4978-93A0-E00120FEE765}
|
||||
{694E763C-E076-4F36-A3CE-3A55D794C871} = {A1786514-35EB-4AFA-87E1-2FB89D748C02}
|
||||
{E18BC5D2-5A32-469A-9E0B-5ACFE3B6639B} = {A1786514-35EB-4AFA-87E1-2FB89D748C02}
|
||||
{1F2CE508-42D0-437B-83AB-0D5FEBE324F8} = {F77CA77C-49CE-49B5-BE5A-FCCF6C63D65A}
|
||||
{77410EA7-739C-475B-B460-CCEF5CEE1AD1} = {F77CA77C-49CE-49B5-BE5A-FCCF6C63D65A}
|
||||
{7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{D2F19D7F-A9DA-47D2-A445-F9ED8D4479C1} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{51EE0C7A-6D1A-4538-957A-34B6906FC932} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{3FD37B50-3C7D-49E9-9456-A3E82675227D} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{5D3D2499-0732-4545-87F5-C26AF1FE827B} = {51EE0C7A-6D1A-4538-957A-34B6906FC932}
|
||||
{EE5ECF6A-0D4F-4737-BAD3-AE66D1B53D83} = {7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7}
|
||||
{0A74D3F2-7814-4FB4-890D-4899749B67A2} = {7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7}
|
||||
{FDA41108-8194-49B2-8F78-9F8AECABB7BC} = {7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7}
|
||||
{B6D11E8B-CAAE-4452-B3AE-A49DA7E68FE0} = {3FD37B50-3C7D-49E9-9456-A3E82675227D}
|
||||
{6D17EFCC-63DB-4E51-8073-25D5E59B2170} = {D2F19D7F-A9DA-47D2-A445-F9ED8D4479C1}
|
||||
{5ED78466-4114-48ED-9A6E-02143984E7A1} = {B85B39B5-B341-4117-8626-C5DD4F375569}
|
||||
{D1907049-C23E-47CB-9DF1-0D9EDB7CE117} = {570274DD-E84A-4F22-9079-D60B59EC9ED2}
|
||||
{5F943131-E273-474E-891E-6386C4B10D00} = {570274DD-E84A-4F22-9079-D60B59EC9ED2}
|
||||
{183FF15D-1B24-4FA4-A5E4-505825919113} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{3CB44FE8-8DC1-49BD-864A-72FB6A8229C5} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{254C235E-7E2D-4FEE-9EB4-50E48BDB1295} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{6C250353-B112-42F5-BBE9-FA2A725870FD} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{99914C87-B2FC-4DB2-9BFC-AA2D63B3024F} = {183FF15D-1B24-4FA4-A5E4-505825919113}
|
||||
{18FAA2C9-5B3B-41D4-83F4-F91B0F2355A0} = {D1907049-C23E-47CB-9DF1-0D9EDB7CE117}
|
||||
{301AB091-1BBB-4D95-9A54-AA7A8EE928EF} = {3CB44FE8-8DC1-49BD-864A-72FB6A8229C5}
|
||||
{3020E2CD-C6E5-4489-914E-96083705AF0E} = {6C250353-B112-42F5-BBE9-FA2A725870FD}
|
||||
{1CD81080-9F44-49AA-94F9-EFEBFD8073E4} = {254C235E-7E2D-4FEE-9EB4-50E48BDB1295}
|
||||
{DB31E41A-D441-4BB8-B96C-F70395FBFB95} = {DBAE70CC-011A-4997-9612-58AFAFF73291}
|
||||
{ECBE72AF-7F47-4086-A3F8-AF011085E253} = {DB31E41A-D441-4BB8-B96C-F70395FBFB95}
|
||||
{EAAC4A89-D71D-426F-ABB0-127C3E777E54} = {DBAE70CC-011A-4997-9612-58AFAFF73291}
|
||||
{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0} = {EAAC4A89-D71D-426F-ABB0-127C3E777E54}
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6538BDF3-A741-46E9-8988-C859ABB2FBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6538BDF3-A741-46E9-8988-C859ABB2FBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
@ -252,5 +207,70 @@ Global
|
||||
{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7B7E65D-DB14-494C-A748-EF90666FB0B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E7B7E65D-DB14-494C-A748-EF90666FB0B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7B7E65D-DB14-494C-A748-EF90666FB0B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E7B7E65D-DB14-494C-A748-EF90666FB0B1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE7E4AE8-4A5C-44AE-B1FC-2A04824EE29B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE7E4AE8-4A5C-44AE-B1FC-2A04824EE29B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE7E4AE8-4A5C-44AE-B1FC-2A04824EE29B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE7E4AE8-4A5C-44AE-B1FC-2A04824EE29B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{57366225-A26A-4715-A690-DA641B0C0450} = {33F5E6F6-FACB-4D34-8E48-B80E0497D239}
|
||||
{39BAB0A0-3C16-49B1-B817-9EC5C600BF5E} = {33F5E6F6-FACB-4D34-8E48-B80E0497D239}
|
||||
{570274DD-E84A-4F22-9079-D60B59EC9ED2} = {F1BEC66C-B321-45D8-95C1-8DCD4743275D}
|
||||
{AD2FB7C1-8641-47E9-B62D-B3A2D74147D8} = {F1BEC66C-B321-45D8-95C1-8DCD4743275D}
|
||||
{DBAE70CC-011A-4997-9612-58AFAFF73291} = {96913068-5EFE-4D13-8B00-924AFED16439}
|
||||
{FB529C7A-F884-42D1-BE41-2A6B7CC4101B} = {96913068-5EFE-4D13-8B00-924AFED16439}
|
||||
{5A2A9F1C-245A-4978-93A0-E00120FEE765} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{D0A6E1E7-DFB6-4940-A254-F2E24897C4AC} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{F77CA77C-49CE-49B5-BE5A-FCCF6C63D65A} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{A1786514-35EB-4AFA-87E1-2FB89D748C02} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{A5E49049-0DFF-4D87-9188-2B0ACBC0D59B} = {57366225-A26A-4715-A690-DA641B0C0450}
|
||||
{6538BDF3-A741-46E9-8988-C859ABB2FBB2} = {A5E49049-0DFF-4D87-9188-2B0ACBC0D59B}
|
||||
{B851799B-A328-4E40-9095-C56C11A6235E} = {D0A6E1E7-DFB6-4940-A254-F2E24897C4AC}
|
||||
{674C0974-11C4-4BF7-B2DF-8ED753919224} = {D0A6E1E7-DFB6-4940-A254-F2E24897C4AC}
|
||||
{FB8AA20B-0D31-4241-A126-07992BCF7E2A} = {5A2A9F1C-245A-4978-93A0-E00120FEE765}
|
||||
{0EB6B262-197D-450C-A56E-634D2D428FCB} = {5A2A9F1C-245A-4978-93A0-E00120FEE765}
|
||||
{694E763C-E076-4F36-A3CE-3A55D794C871} = {A1786514-35EB-4AFA-87E1-2FB89D748C02}
|
||||
{E18BC5D2-5A32-469A-9E0B-5ACFE3B6639B} = {A1786514-35EB-4AFA-87E1-2FB89D748C02}
|
||||
{1F2CE508-42D0-437B-83AB-0D5FEBE324F8} = {F77CA77C-49CE-49B5-BE5A-FCCF6C63D65A}
|
||||
{77410EA7-739C-475B-B460-CCEF5CEE1AD1} = {F77CA77C-49CE-49B5-BE5A-FCCF6C63D65A}
|
||||
{7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{D2F19D7F-A9DA-47D2-A445-F9ED8D4479C1} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{51EE0C7A-6D1A-4538-957A-34B6906FC932} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{3FD37B50-3C7D-49E9-9456-A3E82675227D} = {39BAB0A0-3C16-49B1-B817-9EC5C600BF5E}
|
||||
{5D3D2499-0732-4545-87F5-C26AF1FE827B} = {51EE0C7A-6D1A-4538-957A-34B6906FC932}
|
||||
{EE5ECF6A-0D4F-4737-BAD3-AE66D1B53D83} = {7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7}
|
||||
{0A74D3F2-7814-4FB4-890D-4899749B67A2} = {7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7}
|
||||
{FDA41108-8194-49B2-8F78-9F8AECABB7BC} = {7D81CAA6-6DEF-43C1-AE1B-D0F0B7B09CE7}
|
||||
{B6D11E8B-CAAE-4452-B3AE-A49DA7E68FE0} = {3FD37B50-3C7D-49E9-9456-A3E82675227D}
|
||||
{6D17EFCC-63DB-4E51-8073-25D5E59B2170} = {D2F19D7F-A9DA-47D2-A445-F9ED8D4479C1}
|
||||
{5ED78466-4114-48ED-9A6E-02143984E7A1} = {B85B39B5-B341-4117-8626-C5DD4F375569}
|
||||
{D1907049-C23E-47CB-9DF1-0D9EDB7CE117} = {570274DD-E84A-4F22-9079-D60B59EC9ED2}
|
||||
{5F943131-E273-474E-891E-6386C4B10D00} = {570274DD-E84A-4F22-9079-D60B59EC9ED2}
|
||||
{183FF15D-1B24-4FA4-A5E4-505825919113} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{3CB44FE8-8DC1-49BD-864A-72FB6A8229C5} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{254C235E-7E2D-4FEE-9EB4-50E48BDB1295} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{6C250353-B112-42F5-BBE9-FA2A725870FD} = {5F943131-E273-474E-891E-6386C4B10D00}
|
||||
{99914C87-B2FC-4DB2-9BFC-AA2D63B3024F} = {183FF15D-1B24-4FA4-A5E4-505825919113}
|
||||
{18FAA2C9-5B3B-41D4-83F4-F91B0F2355A0} = {D1907049-C23E-47CB-9DF1-0D9EDB7CE117}
|
||||
{301AB091-1BBB-4D95-9A54-AA7A8EE928EF} = {3CB44FE8-8DC1-49BD-864A-72FB6A8229C5}
|
||||
{3020E2CD-C6E5-4489-914E-96083705AF0E} = {6C250353-B112-42F5-BBE9-FA2A725870FD}
|
||||
{1CD81080-9F44-49AA-94F9-EFEBFD8073E4} = {254C235E-7E2D-4FEE-9EB4-50E48BDB1295}
|
||||
{DB31E41A-D441-4BB8-B96C-F70395FBFB95} = {DBAE70CC-011A-4997-9612-58AFAFF73291}
|
||||
{ECBE72AF-7F47-4086-A3F8-AF011085E253} = {DB31E41A-D441-4BB8-B96C-F70395FBFB95}
|
||||
{EAAC4A89-D71D-426F-ABB0-127C3E777E54} = {DBAE70CC-011A-4997-9612-58AFAFF73291}
|
||||
{E7BA185C-B26D-4ACB-A24A-70AB730DD2A0} = {EAAC4A89-D71D-426F-ABB0-127C3E777E54}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {AD2FB7C1-8641-47E9-B62D-B3A2D74147D8}
|
||||
{E7B7E65D-DB14-494C-A748-EF90666FB0B1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{AE7E4AE8-4A5C-44AE-B1FC-2A04824EE29B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BF29DFD4-25EC-44C4-9DA6-E3AC4B292257}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user