From 0345f58f7b3fcf96287658c57d5d41e521ad961d Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Wed, 30 Apr 2025 17:29:40 +0300 Subject: [PATCH] add city entity management --- src/Application/Cities/CityDto.cs | 39 + .../Cities/Commands/AddCity/AddCityCommand.cs | 10 + .../AddCity/AddCityCommandAuthorizer.cs | 31 + .../Commands/AddCity/AddCityCommandHandler.cs | 61 + .../AddCity/AddCityCommandValidator.cs | 27 + .../Commands/DeleteCity/DeleteCityCommand.cs | 8 + .../DeleteCity/DeleteCityCommandAuthorizer.cs | 31 + .../DeleteCity/DeleteCityCommandHandler.cs | 34 + .../DeleteCity/DeleteCityCommandValidator.cs | 14 + .../Commands/UpdateCity/UpdateCityCommand.cs | 12 + .../UpdateCity/UpdateCityCommandAuthorizer.cs | 31 + .../UpdateCity/UpdateCityCommandHandler.cs | 55 + .../UpdateCity/UpdateCityCommandValidator.cs | 31 + .../GetCitiesPage/GetCitiesPageQuery.cs | 19 + .../GetCitiesPageQueryAuthorizer.cs | 31 + .../GetCitiesPageQueryHandler.cs | 55 + .../GetCitiesPageQueryValidator.cs | 43 + .../Cities/Queries/GetCity/GetCityQuery.cs | 8 + .../Queries/GetCity/GetCityQueryAuthorizer.cs | 31 + .../Queries/GetCity/GetCityQueryHandler.cs | 39 + .../Queries/GetCity/GetCityQueryValidator.cs | 14 + .../GetCitiesPageFilterViewModel.cs | 8 + .../Repositories/CityRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 4 + .../Persistence/Configuration.cs | 9 +- src/HttpApi/Controllers/CitiesController.cs | 176 ++ src/HttpApi/Controllers/RegionsController.cs | 6 - .../Configurations/BaseConfiguration.cs | 0 .../Configurations/CountryConfiguration.cs | 0 .../Configurations/RegionConfiguration.cs | 0 src/Persistence/InMemory/InMemoryDbContext.cs | 12 +- .../InMemory/InMemoryUnitOfWork.cs | 4 + .../Repositories/InMemoryCityRepository.cs | 11 + .../Configurations/AddressConfiguration.cs | 55 - .../Configurations/CityConfiguration.cs | 8 - .../RouteAddressConfiguration.cs | 83 - .../Configurations/RouteConfiguration.cs | 30 - ...3338_Countries_Regions_Cities.Designer.cs} | 141 +- ...0250430113338_Countries_Regions_Cities.cs} | 166 +- .../PostgreSqlDbContextModelSnapshot.cs | 137 +- .../PostgreSql/PostgreSqlUnitOfWork.cs | 4 + .../Repositories/PostgreSqlCityRepository.cs | 11 + .../CitiesTests.cs | 1576 +++++++++++++++++ .../RegionsTests.cs | 8 +- 44 files changed, 2544 insertions(+), 535 deletions(-) create mode 100644 src/Application/Cities/CityDto.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommand.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs create mode 100644 src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs create mode 100644 src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs create mode 100644 src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs create mode 100644 src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQuery.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs create mode 100644 src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs create mode 100644 src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs create mode 100644 src/HttpApi/Controllers/CitiesController.cs rename src/Persistence/{InMemory => }/Configurations/BaseConfiguration.cs (100%) rename src/Persistence/{InMemory => }/Configurations/CountryConfiguration.cs (100%) rename src/Persistence/{InMemory => }/Configurations/RegionConfiguration.cs (100%) create mode 100644 src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs delete mode 100644 src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs delete mode 100644 src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs delete mode 100644 src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs rename src/Persistence/PostgreSql/Migrations/{20250427160059_Initial_migration.Designer.cs => 20250430113338_Countries_Regions_Cities.Designer.cs} (65%) rename src/Persistence/PostgreSql/Migrations/{20250427160059_Initial_migration.cs => 20250430113338_Countries_Regions_Cities.cs} (60%) create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs create mode 100644 tst/Application.IntegrationTests/CitiesTests.cs diff --git a/src/Application/Cities/CityDto.cs b/src/Application/Cities/CityDto.cs new file mode 100644 index 0000000..f3a47ff --- /dev/null +++ b/src/Application/Cities/CityDto.cs @@ -0,0 +1,39 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Cities; + +public sealed class CityDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid CountryUuid { get; set; } + + public string CountryName { get; set; } + + public Guid RegionUuid { get; set; } + + public string RegionName { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)) + .ForMember( + d => d.CountryUuid, + opt => opt.MapFrom(s => s.Region.Country.Guid)) + .ForMember( + d => d.CountryName, + opt => opt.MapFrom(s => s.Region.Country.Name)) + .ForMember( + d => d.RegionUuid, + opt => opt.MapFrom(s => s.Region.Guid)) + .ForMember( + d => d.RegionName, + opt => opt.MapFrom(s => s.Region.Name)); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommand.cs b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs new file mode 100644 index 0000000..3530aec --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public record AddCityCommand : IRequest +{ + public string Name { get; set; } + + public Guid RegionUuid { get; set; } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs new file mode 100644 index 0000000..1122f5f --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs new file mode 100644 index 0000000..fa93931 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs @@ -0,0 +1,61 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddCityCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Name == request.Name && e.Region.Guid == request.RegionUuid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "City with given name already exists."); + } + + var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.RegionUuid, e => e.Country, + cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.RegionUuid} not found."); + } + + entity = new City() + { + Name = request.Name, + RegionId = parentEntity.Id + }; + + entity = await _unitOfWork.CityRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs new file mode 100644 index 0000000..e764345 --- /dev/null +++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; + +public class AddCityCommandValidator : AbstractValidator +{ + public AddCityCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.RegionUuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs new file mode 100644 index 0000000..a8ee4d0 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public record DeleteCityCommand : IRequest +{ + public Guid Uuid { get; set; } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs new file mode 100644 index 0000000..ed62a53 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs new file mode 100644 index 0000000..26dc751 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteCityCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Uuid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CityRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs new file mode 100644 index 0000000..292d274 --- /dev/null +++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; + +public class DeleteCityCommandValidator : AbstractValidator +{ + public DeleteCityCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs new file mode 100644 index 0000000..992cfc1 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public record UpdateCityCommand : IRequest +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid RegionUuid { get; set; } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs new file mode 100644 index 0000000..ed1e198 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateCityCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateCityCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs new file mode 100644 index 0000000..67a788f --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateCityCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateCityCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Uuid, e => e.Region.Country, + cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.RegionUuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.RegionUuid} not found."); + } + + entity.Name = request.Name; + entity.RegionId = parentEntity.Id; + + entity = await _unitOfWork.CityRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs new file mode 100644 index 0000000..c70d946 --- /dev/null +++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; + +public class UpdateCityCommandValidator : AbstractValidator +{ + public UpdateCityCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.RegionUuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs new file mode 100644 index 0000000..23e2c7d --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs @@ -0,0 +1,19 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public record GetCitiesPageQuery : IRequest> +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string Search { get; set; } = String.Empty; + + public string Sort { get; set; } = String.Empty; + + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs new file mode 100644 index 0000000..84872fe --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCitiesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCitiesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs new file mode 100644 index 0000000..91b5daa --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs @@ -0,0 +1,55 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCitiesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetCitiesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.CityRepository.GetPageAsync( + e => + (e.Name.ToLower().Contains(request.Search.ToLower()) || + e.Region.Name.ToLower().Contains(request.Search.ToLower()) || + e.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.RegionUuid != null + ? e.Region.Guid == request.RegionUuid + : true) && + (request.CountryUuid != null + ? e.Region.Country.Guid == request.CountryUuid + : true), + e => e.Region.Country, + request.PageNumber, request.PageSize, + cancellationToken); + + var mappedItems = _mapper + .ProjectTo(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs new file mode 100644 index 0000000..dde8a20 --- /dev/null +++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + +public class GetCitiesPageQueryValidator : AbstractValidator +{ + public GetCitiesPageQueryValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.PageNumber) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)); + + RuleFor(v => v.PageSize) + .GreaterThanOrEqualTo(1) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.GreaterThanOrEqualTo"], + 1)) + .LessThanOrEqualTo(50) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.LessThanOrEqualTo"], + 50)); + + RuleFor(v => v.Search) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQuery.cs b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs new file mode 100644 index 0000000..1c6f9bd --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public record GetCityQuery : IRequest +{ + public Guid Uuid { get; set; } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs new file mode 100644 index 0000000..55f927d --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCityQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCityQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs new file mode 100644 index 0000000..954f646 --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCityQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetCityQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CityRepository.GetOneAsync( + e => e.Guid == request.Uuid, e => e.Region.Country, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs new file mode 100644 index 0000000..894166b --- /dev/null +++ b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; + +public class GetCityQueryValidator : AbstractValidator +{ + public GetCityQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs new file mode 100644 index 0000000..68ec4d9 --- /dev/null +++ b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Cities.ViewModels; + +public sealed class GetCitiesPageFilterViewModel +{ + public Guid? CountryUuid { get; set; } + + public Guid? RegionUuid { get; set; } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs new file mode 100644 index 0000000..beee75b --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/CityRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface CityRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index b98a962..9c34925 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -5,8 +5,12 @@ namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; public interface UnitOfWork : IDisposable { CountryRepository CountryRepository { get; } + RegionRepository RegionRepository { get; } + CityRepository CityRepository { get; } + int Save(); + Task SaveAsync(CancellationToken cancellationToken); } diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs index 5aacbf3..1eb83be 100644 --- a/src/Configuration/Persistence/Configuration.cs +++ b/src/Configuration/Persistence/Configuration.cs @@ -72,9 +72,12 @@ public static class Configuration $"{configuration.Type} datastore is not supported."); } - // using var serviceProvider = services.BuildServiceProvider(); - // var unitOfWork = serviceProvider.GetService(); - // DbSeeder.Seed(unitOfWork); + if (configuration.Seed) + { + using var serviceProvider = services.BuildServiceProvider(); + var unitOfWork = serviceProvider.GetService(); + DbSeeder.Seed(unitOfWork); + } return services; } diff --git a/src/HttpApi/Controllers/CitiesController.cs b/src/HttpApi/Controllers/CitiesController.cs new file mode 100644 index 0000000..5286fba --- /dev/null +++ b/src/HttpApi/Controllers/CitiesController.cs @@ -0,0 +1,176 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Cities; +using cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; +using cuqmbr.TravelGuide.Application.Cities.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("cities")] +public class CitiesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Create a city")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddCityCommand command, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send(command, cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all cities")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetCitiesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CountryUuid = filterQuery.CountryUuid, + RegionUuid = filterQuery.RegionUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get city by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetCityQuery() { Uuid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update city")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateCityCommand command, + CancellationToken cancellationToken) + { + command.Uuid = uuid; + return await Mediator.Send(command, cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete city")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CityDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteCityCommand() { Uuid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/RegionsController.cs b/src/HttpApi/Controllers/RegionsController.cs index 1306916..4b5a72d 100644 --- a/src/HttpApi/Controllers/RegionsController.cs +++ b/src/HttpApi/Controllers/RegionsController.cs @@ -53,9 +53,6 @@ public class RegionsController : ControllerBase [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(PaginatedList))] - [SwaggerResponse( - StatusCodes.Status400BadRequest, "Object already exists", - typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", typeof(HttpValidationProblemDetails))] @@ -91,9 +88,6 @@ public class RegionsController : ControllerBase [SwaggerOperation("Get region by uuid")] [SwaggerResponse( StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] - [SwaggerResponse( - StatusCodes.Status400BadRequest, "Object already exists", - typeof(ProblemDetails))] [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error", typeof(HttpValidationProblemDetails))] diff --git a/src/Persistence/InMemory/Configurations/BaseConfiguration.cs b/src/Persistence/Configurations/BaseConfiguration.cs similarity index 100% rename from src/Persistence/InMemory/Configurations/BaseConfiguration.cs rename to src/Persistence/Configurations/BaseConfiguration.cs diff --git a/src/Persistence/InMemory/Configurations/CountryConfiguration.cs b/src/Persistence/Configurations/CountryConfiguration.cs similarity index 100% rename from src/Persistence/InMemory/Configurations/CountryConfiguration.cs rename to src/Persistence/Configurations/CountryConfiguration.cs diff --git a/src/Persistence/InMemory/Configurations/RegionConfiguration.cs b/src/Persistence/Configurations/RegionConfiguration.cs similarity index 100% rename from src/Persistence/InMemory/Configurations/RegionConfiguration.cs rename to src/Persistence/Configurations/RegionConfiguration.cs diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 6c74ec5..0c788c7 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,5 +1,6 @@ -using System.Reflection; +// using System.Reflection; // using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; // using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -10,6 +11,10 @@ public class InMemoryDbContext : DbContext public InMemoryDbContext(DbContextOptions options) : base(options) { } + public DbSet Countries { get => Set(); } + public DbSet Regions { get => Set(); } + public DbSet Cities { get => Set(); } + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -18,11 +23,6 @@ public class InMemoryDbContext : DbContext // "vehicle_type", // VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); // - builder - .ApplyConfigurationsFromAssembly( - Assembly.GetExecutingAssembly(), - t => t.Namespace == - "cuqmbr.TravelGuide.Persistence.InMemory.Configurations"); } protected override void ConfigureConventions( diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 23af267..974c1df 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -15,11 +15,15 @@ public sealed class InMemoryUnitOfWork : UnitOfWork CountryRepository = new InMemoryCountryRepository(_dbContext); RegionRepository = new InMemoryRegionRepository(_dbContext); + CityRepository = new InMemoryCityRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + public CityRepository CityRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs new file mode 100644 index 0000000..22f942a --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCityRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryCityRepository : + InMemoryBaseRepository, CityRepository +{ + public InMemoryCityRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs deleted file mode 100644 index 2efda70..0000000 --- a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; - -public class AddressConfiguration : BaseConfiguration
-{ - public override void Configure(EntityTypeBuilder
builder) - { - builder - .ToTable("addresses"); - - base.Configure(builder); - - - builder - .Property(a => a.Name) - .HasColumnName("name") - .HasColumnType("varchar(128)") - .IsRequired(true); - - // builder - // .Property(a => a.VehicleType) - // .HasColumnName("vehicle_type") - // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") - // .HasConversion() - // .IsRequired(true); - - - builder - .Property(a => a.CityId) - .HasColumnName("city_id") - .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(a => a.City) - .WithMany(c => c.Addresses) - .HasForeignKey(a => a.CityId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(a => a.CityId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") - .IsUnique(); - } -} diff --git a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs index dc6114c..589d652 100644 --- a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs +++ b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs @@ -36,13 +36,5 @@ public class CityConfiguration : BaseConfiguration $"{builder.Metadata.GetTableName()}_" + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(c => c.RegionId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") - .IsUnique(); } } diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs deleted file mode 100644 index 547d410..0000000 --- a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs +++ /dev/null @@ -1,83 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; - -public class RouteAddressConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("route_addresses"); - - base.Configure(builder); - - - builder - .Property(ra => ra.Order) - .HasColumnName("order") - .HasColumnType("smallint") - .IsRequired(true); - - - builder - .Property(ra => ra.AddressId) - .HasColumnName("address_id") - .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(ra => ra.Address) - .WithMany(a => a.AddressRoutes) - .HasForeignKey(ra => ra.AddressId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(ra => ra.AddressId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") - .IsUnique(); - - - builder - .Property(ra => ra.RouteId) - .HasColumnName("route_id") - .HasColumnType("bigint") - .IsRequired(true); - - builder - .HasOne(ra => ra.Route) - .WithMany(a => a.RouteAddresses) - .HasForeignKey(ra => ra.RouteId) - .HasConstraintName( - "fk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") - .OnDelete(DeleteBehavior.Cascade); - - builder - .HasIndex(ra => ra.RouteId) - .HasDatabaseName( - "ix_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") - .IsUnique(); - - - builder - .HasAlternateKey(ra => new { ra.AddressId, ra.RouteId, ra.Order }) - .HasName( - "altk_" + - $"{builder.Metadata.GetTableName()}_" + - $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}_" + - $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}_" + - $"{builder.Property(ra => ra.Order).Metadata.GetColumnName()}"); - } -} diff --git a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs deleted file mode 100644 index 6c81f2e..0000000 --- a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs +++ /dev/null @@ -1,30 +0,0 @@ -using cuqmbr.TravelGuide.Domain.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; - -public class RouteConfiguration : BaseConfiguration -{ - public override void Configure(EntityTypeBuilder builder) - { - builder - .ToTable("routes"); - - base.Configure(builder); - - - builder - .Property(r => r.Name) - .HasColumnName("name") - .HasColumnType("varchar(64)") - .IsRequired(true); - - // builder - // .Property(r => r.VehicleType) - // .HasColumnName("vehicle_type") - // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") - // .HasConversion() - // .IsRequired(true); - } -} diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs similarity index 65% rename from src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs rename to src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs index d68963c..d7efad4 100644 --- a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.Designer.cs @@ -12,8 +12,8 @@ using cuqmbr.TravelGuide.Persistence.PostgreSql; namespace Persistence.PostgreSql.Migrations { [DbContext(typeof(PostgreSqlDbContext))] - [Migration("20250427160059_Initial_migration")] - partial class Initial_migration + [Migration("20250430113338_Countries_Regions_Cities")] + partial class Countries_Regions_Cities { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -27,65 +27,35 @@ namespace Persistence.PostgreSql.Migrations NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.HasSequence("addresses_id_sequence"); - modelBuilder.HasSequence("cities_id_sequence"); modelBuilder.HasSequence("countries_id_sequence"); modelBuilder.HasSequence("regions_id_sequence"); - modelBuilder.HasSequence("route_addresses_id_sequence"); - - modelBuilder.HasSequence("routes_id_sequence"); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CityId") - .HasColumnType("bigint") - .HasColumnName("city_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(128)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_addresses"); + b.HasIndex("CityId"); - b.HasAlternateKey("Guid") - .HasName("altk_addresses_Guid"); - - b.HasIndex("CityId") - .IsUnique() - .HasDatabaseName("ix_addresses_city_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_addresses_id"); - - b.ToTable("addresses", "application"); + b.ToTable("Address", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => @@ -125,9 +95,7 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_cities_id"); - b.HasIndex("RegionId") - .IsUnique() - .HasDatabaseName("ix_cities_region_id"); + b.HasIndex("RegionId"); b.ToTable("cities", "application"); }); @@ -214,95 +182,49 @@ namespace Persistence.PostgreSql.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(64)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_routes"); - - b.HasAlternateKey("Guid") - .HasName("altk_routes_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_routes_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_routes_id"); - - b.ToTable("routes", "application"); + b.ToTable("Route", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AddressId") - .HasColumnType("bigint") - .HasColumnName("address_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Order") - .HasColumnType("smallint") - .HasColumnName("order"); + .HasColumnType("smallint"); b.Property("RouteId") - .HasColumnType("bigint") - .HasColumnName("route_id"); + .HasColumnType("bigint"); - b.HasKey("Id") - .HasName("pk_route_addresses"); + b.HasKey("Id"); - b.HasAlternateKey("Guid") - .HasName("altk_route_addresses_Guid"); + b.HasIndex("AddressId"); - b.HasAlternateKey("AddressId", "RouteId", "Order") - .HasName("altk_route_addresses_address_id_route_id_order"); + b.HasIndex("RouteId"); - b.HasIndex("AddressId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_address_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_route_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_route_addresses_id"); - - b.HasIndex("RouteId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_route_id"); - - b.ToTable("route_addresses", "application"); + b.ToTable("RouteAddress", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => @@ -311,8 +233,7 @@ namespace Persistence.PostgreSql.Migrations .WithMany("Addresses") .HasForeignKey("CityId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_addresses_city_id"); + .IsRequired(); b.Navigation("City"); }); @@ -347,15 +268,13 @@ namespace Persistence.PostgreSql.Migrations .WithMany("AddressRoutes") .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_address_id"); + .IsRequired(); b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") .WithMany("RouteAddresses") .HasForeignKey("RouteId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_route_id"); + .IsRequired(); b.Navigation("Address"); diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs similarity index 60% rename from src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs rename to src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs index 2e209c1..04b8098 100644 --- a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs +++ b/src/Persistence/PostgreSql/Migrations/20250430113338_Countries_Regions_Cities.cs @@ -1,12 +1,13 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace Persistence.PostgreSql.Migrations { /// - public partial class Initial_migration : Migration + public partial class Countries_Regions_Cities : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -17,10 +18,6 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.AlterDatabase() .Annotation("Npgsql:Enum:vehicle_type", "bus,train,aircraft"); - migrationBuilder.CreateSequence( - name: "addresses_id_sequence", - schema: "application"); - migrationBuilder.CreateSequence( name: "cities_id_sequence", schema: "application"); @@ -33,14 +30,6 @@ namespace Persistence.PostgreSql.Migrations name: "regions_id_sequence", schema: "application"); - migrationBuilder.CreateSequence( - name: "route_addresses_id_sequence", - schema: "application"); - - migrationBuilder.CreateSequence( - name: "routes_id_sequence", - schema: "application"); - migrationBuilder.CreateTable( name: "countries", schema: "application", @@ -57,19 +46,18 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "routes", + name: "Route", schema: "application", columns: table => new { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"), - name = table.Column(type: "varchar(64)", nullable: false), - vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Guid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("pk_routes", x => x.id); - table.UniqueConstraint("altk_routes_Guid", x => x.uuid); + table.PrimaryKey("PK_Route", x => x.Id); }); migrationBuilder.CreateTable( @@ -119,23 +107,22 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "addresses", + name: "Address", schema: "application", columns: table => new { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.addresses_id_sequence')"), - name = table.Column(type: "varchar(128)", nullable: false), - vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), - city_id = table.Column(type: "bigint", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + CityId = table.Column(type: "bigint", nullable: false), + Guid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("pk_addresses", x => x.id); - table.UniqueConstraint("altk_addresses_Guid", x => x.uuid); + table.PrimaryKey("PK_Address", x => x.Id); table.ForeignKey( - name: "fk_addresses_city_id", - column: x => x.city_id, + name: "FK_Address_cities_CityId", + column: x => x.CityId, principalSchema: "application", principalTable: "cities", principalColumn: "id", @@ -143,57 +130,41 @@ namespace Persistence.PostgreSql.Migrations }); migrationBuilder.CreateTable( - name: "route_addresses", + name: "RouteAddress", schema: "application", columns: table => new { - id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"), - order = table.Column(type: "smallint", nullable: false), - address_id = table.Column(type: "bigint", nullable: false), - route_id = table.Column(type: "bigint", nullable: false), - uuid = table.Column(type: "uuid", nullable: false) + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Order = table.Column(type: "smallint", nullable: false), + AddressId = table.Column(type: "bigint", nullable: false), + RouteId = table.Column(type: "bigint", nullable: false), + Guid = table.Column(type: "uuid", nullable: false) }, constraints: table => { - table.PrimaryKey("pk_route_addresses", x => x.id); - table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order }); - table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid); + table.PrimaryKey("PK_RouteAddress", x => x.Id); table.ForeignKey( - name: "fk_route_addresses_address_id", - column: x => x.address_id, + name: "FK_RouteAddress_Address_AddressId", + column: x => x.AddressId, principalSchema: "application", - principalTable: "addresses", - principalColumn: "id", + principalTable: "Address", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "fk_route_addresses_route_id", - column: x => x.route_id, + name: "FK_RouteAddress_Route_RouteId", + column: x => x.RouteId, principalSchema: "application", - principalTable: "routes", - principalColumn: "id", + principalTable: "Route", + principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( - name: "ix_addresses_city_id", + name: "IX_Address_CityId", schema: "application", - table: "addresses", - column: "city_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_addresses_id", - schema: "application", - table: "addresses", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_addresses_uuid", - schema: "application", - table: "addresses", - column: "uuid", - unique: true); + table: "Address", + column: "CityId"); migrationBuilder.CreateIndex( name: "ix_cities_id", @@ -203,11 +174,10 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "ix_cities_region_id", + name: "IX_cities_region_id", schema: "application", table: "cities", - column: "region_id", - unique: true); + column: "region_id"); migrationBuilder.CreateIndex( name: "ix_cities_uuid", @@ -251,61 +221,31 @@ namespace Persistence.PostgreSql.Migrations unique: true); migrationBuilder.CreateIndex( - name: "ix_route_addresses_address_id", + name: "IX_RouteAddress_AddressId", schema: "application", - table: "route_addresses", - column: "address_id", - unique: true); + table: "RouteAddress", + column: "AddressId"); migrationBuilder.CreateIndex( - name: "ix_route_addresses_id", + name: "IX_RouteAddress_RouteId", schema: "application", - table: "route_addresses", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_route_id", - schema: "application", - table: "route_addresses", - column: "route_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_route_addresses_uuid", - schema: "application", - table: "route_addresses", - column: "uuid", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_routes_id", - schema: "application", - table: "routes", - column: "id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_routes_uuid", - schema: "application", - table: "routes", - column: "uuid", - unique: true); + table: "RouteAddress", + column: "RouteId"); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "route_addresses", + name: "RouteAddress", schema: "application"); migrationBuilder.DropTable( - name: "addresses", + name: "Address", schema: "application"); migrationBuilder.DropTable( - name: "routes", + name: "Route", schema: "application"); migrationBuilder.DropTable( @@ -320,10 +260,6 @@ namespace Persistence.PostgreSql.Migrations name: "countries", schema: "application"); - migrationBuilder.DropSequence( - name: "addresses_id_sequence", - schema: "application"); - migrationBuilder.DropSequence( name: "cities_id_sequence", schema: "application"); @@ -335,14 +271,6 @@ namespace Persistence.PostgreSql.Migrations migrationBuilder.DropSequence( name: "regions_id_sequence", schema: "application"); - - migrationBuilder.DropSequence( - name: "route_addresses_id_sequence", - schema: "application"); - - migrationBuilder.DropSequence( - name: "routes_id_sequence", - schema: "application"); } } } diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 9e89c0f..1d282f6 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -24,65 +24,35 @@ namespace Persistence.PostgreSql.Migrations NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.HasSequence("addresses_id_sequence"); - modelBuilder.HasSequence("cities_id_sequence"); modelBuilder.HasSequence("countries_id_sequence"); modelBuilder.HasSequence("regions_id_sequence"); - modelBuilder.HasSequence("route_addresses_id_sequence"); - - modelBuilder.HasSequence("routes_id_sequence"); - modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("CityId") - .HasColumnType("bigint") - .HasColumnName("city_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(128)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_addresses"); + b.HasIndex("CityId"); - b.HasAlternateKey("Guid") - .HasName("altk_addresses_Guid"); - - b.HasIndex("CityId") - .IsUnique() - .HasDatabaseName("ix_addresses_city_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_addresses_id"); - - b.ToTable("addresses", "application"); + b.ToTable("Address", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => @@ -122,9 +92,7 @@ namespace Persistence.PostgreSql.Migrations .IsUnique() .HasDatabaseName("ix_cities_id"); - b.HasIndex("RegionId") - .IsUnique() - .HasDatabaseName("ix_cities_region_id"); + b.HasIndex("RegionId"); b.ToTable("cities", "application"); }); @@ -211,95 +179,49 @@ namespace Persistence.PostgreSql.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(64)") - .HasColumnName("name"); + .HasColumnType("text"); - b.Property("VehicleType") - .IsRequired() - .HasColumnType("application.vehicle_type") - .HasColumnName("vehicle_type"); + b.HasKey("Id"); - b.HasKey("Id") - .HasName("pk_routes"); - - b.HasAlternateKey("Guid") - .HasName("altk_routes_Guid"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_routes_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_routes_id"); - - b.ToTable("routes", "application"); + b.ToTable("Route", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id") - .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AddressId") - .HasColumnType("bigint") - .HasColumnName("address_id"); + .HasColumnType("bigint"); b.Property("Guid") - .HasColumnType("uuid") - .HasColumnName("uuid"); + .HasColumnType("uuid"); b.Property("Order") - .HasColumnType("smallint") - .HasColumnName("order"); + .HasColumnType("smallint"); b.Property("RouteId") - .HasColumnType("bigint") - .HasColumnName("route_id"); + .HasColumnType("bigint"); - b.HasKey("Id") - .HasName("pk_route_addresses"); + b.HasKey("Id"); - b.HasAlternateKey("Guid") - .HasName("altk_route_addresses_Guid"); + b.HasIndex("AddressId"); - b.HasAlternateKey("AddressId", "RouteId", "Order") - .HasName("altk_route_addresses_address_id_route_id_order"); + b.HasIndex("RouteId"); - b.HasIndex("AddressId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_address_id"); - - b.HasIndex("Guid") - .IsUnique() - .HasDatabaseName("ix_route_addresses_uuid"); - - b.HasIndex("Id") - .IsUnique() - .HasDatabaseName("ix_route_addresses_id"); - - b.HasIndex("RouteId") - .IsUnique() - .HasDatabaseName("ix_route_addresses_route_id"); - - b.ToTable("route_addresses", "application"); + b.ToTable("RouteAddress", "application"); }); modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => @@ -308,8 +230,7 @@ namespace Persistence.PostgreSql.Migrations .WithMany("Addresses") .HasForeignKey("CityId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_addresses_city_id"); + .IsRequired(); b.Navigation("City"); }); @@ -344,15 +265,13 @@ namespace Persistence.PostgreSql.Migrations .WithMany("AddressRoutes") .HasForeignKey("AddressId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_address_id"); + .IsRequired(); b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") .WithMany("RouteAddresses") .HasForeignKey("RouteId") .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_route_addresses_route_id"); + .IsRequired(); b.Navigation("Address"); diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 320b7f2..ae0977a 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -15,11 +15,15 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork CountryRepository = new PostgreSqlCountryRepository(_dbContext); RegionRepository = new PostgreSqlRegionRepository(_dbContext); + CityRepository = new PostgreSqlCityRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + public CityRepository CityRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs new file mode 100644 index 0000000..441d6b9 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCityRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlCityRepository : + PostgreSqlBaseRepository, CityRepository +{ + public PostgreSqlCityRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/tst/Application.IntegrationTests/CitiesTests.cs b/tst/Application.IntegrationTests/CitiesTests.cs new file mode 100644 index 0000000..3554879 --- /dev/null +++ b/tst/Application.IntegrationTests/CitiesTests.cs @@ -0,0 +1,1576 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; +using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; +using cuqmbr.TravelGuide.Application.Cities.Commands.AddCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity; +using cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCity; +using cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage; + + +namespace cuqmbr.TravelGuide.Application.IntegrationTests; + +public class CitiesTests : TestBase +{ + [Fact] + public async Task AddCity_WithAdminRole_CityAdded() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult); + + Assert.NotNull(getCityResult.Name); + Assert.Equal(cityName, getCityResult.Name); + + Assert.NotNull(getCityResult.CountryName); + Assert.Equal(countryName, getCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult.CountryUuid); + + Assert.NotNull(getCityResult.RegionName); + Assert.Equal(regionName, getCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, getCityResult.RegionUuid); + } + + [Fact] + public async Task + AddDuplicateCity_WithAdminRole_ThrowsDuplicateEntityException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + AddSameCitiesToDifferentRegions_WithAdminRole_CitiesAdded() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult1 = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult1.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult1); + + Assert.NotNull(getCityResult1.Name); + Assert.Equal(cityName, getCityResult1.Name); + + Assert.NotNull(getCityResult1.CountryName); + Assert.Equal(countryName, getCityResult1.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult1.CountryUuid); + + Assert.NotNull(getCityResult1.RegionName); + Assert.Equal(regionName1, getCityResult1.RegionName); + Assert.Equal(addRegionResult1.Uuid, getCityResult1.RegionUuid); + + var getCityResult2 = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult2.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult2); + + Assert.NotNull(getCityResult2.Name); + Assert.Equal(cityName, getCityResult2.Name); + + Assert.NotNull(getCityResult2.CountryName); + Assert.Equal(countryName, getCityResult2.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult2.CountryUuid); + + Assert.NotNull(getCityResult2.RegionName); + Assert.Equal(regionName2, getCityResult2.RegionName); + Assert.Equal(addRegionResult2.Uuid, getCityResult2.RegionUuid); + } + + [Fact] + public async Task + AddCity_WithNonExistentRegionUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = "Name", + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + AddCity_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + AddCity_WithInvalidRegionUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCityCommand() + { + Name = "Name", + RegionUuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task AddCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new AddCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task AddCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new AddCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task UpdateCity_WithAdminRole_CityUpdated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "City Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + string newName = "Different Name"; + + var updateCityResult = await mediator.Send( + new UpdateCityCommand() + { + Uuid = addCityResult.Uuid, + Name = newName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(updateCityResult); + + Assert.NotNull(updateCityResult.Name); + Assert.Equal(newName, updateCityResult.Name); + Assert.Equal(addCityResult.Uuid, updateCityResult.Uuid); + + Assert.NotNull(updateCityResult.CountryName); + Assert.Equal(countryName, updateCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, updateCityResult.CountryUuid); + + Assert.NotNull(updateCityResult.RegionName); + Assert.Equal(regionName, updateCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, updateCityResult.RegionUuid); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + UpdateCity_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Name = name, + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCity_WithInvalidRegionUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = Guid.NewGuid(), + Name = "Name", + RegionUuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Name = "Name", + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = Guid.NewGuid(), + Name = "Different Name", + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCity_WithNonExistentRegionUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = "Name", + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCityCommand() + { + Uuid = addCityResult.Uuid, + Name = "Different Name", + RegionUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new UpdateCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task UpdateCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new UpdateCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task DeleteCity_WithAdminRole_CityDeleted() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = "Name", + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + await mediator.Send( + new DeleteCityCommand() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCityQuery() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + DeleteCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCityCommand() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + DeleteCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCityCommand() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task DeleteCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new DeleteCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task DeleteCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new DeleteCityCommand(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetCity_WithAdminRole_CityReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Name"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Name"; + + var addRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName = "Name"; + + var addCityResult = await mediator.Send( + new AddCityCommand() + { + Name = cityName, + RegionUuid = addRegionResult.Uuid + }, TestContext.Current.CancellationToken); + + var getCityResult = await mediator.Send( + new GetCityQuery() + { + Uuid = addCityResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCityResult); + + Assert.NotNull(getCityResult.Name); + Assert.Equal(cityName, getCityResult.Name); + + Assert.NotNull(getCityResult.CountryName); + Assert.Equal(countryName, getCityResult.CountryName); + Assert.Equal(addCountryResult.Uuid, getCityResult.CountryUuid); + + Assert.NotNull(getCityResult.RegionName); + Assert.Equal(regionName, getCityResult.RegionName); + Assert.Equal(addRegionResult.Uuid, getCityResult.RegionUuid); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + GetCity_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + GetCity_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCity_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetCity_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetCitiesPage_WithAdminRole_CitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name 1"; + + var addCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "RegionName 1"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(2, getCitiesResult.TotalPages); + Assert.True(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + + + getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 2, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(2, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(2, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.True(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + + + getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Equal(2, getCitiesResult.Items.Count()); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + + + Assert.NotNull(getCitiesResult.Items.Last()); + + Assert.NotNull(getCitiesResult.Items.Last().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.Last().Name); + + Assert.NotNull(getCitiesResult.Items.Last().CountryName); + Assert.Equal( + countryName, getCitiesResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult.Uuid, getCitiesResult.Items.Last().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.Last().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.Last().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.Last().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByCountryName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "RegionName 1"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cOuNtRy nAme 1" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByRegionName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "reGioN nAme 2" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSearchByCityName_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "cItY nAme 2" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithSort_WithAdminRole_SortedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "-countryName" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(2, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Equal(2, getCitiesResult.Items.Count()); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + + + Assert.NotNull(getCitiesResult.Items.Last()); + + Assert.NotNull(getCitiesResult.Items.Last().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.Last().Name); + + Assert.NotNull(getCitiesResult.Items.Last().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.Last().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.Last().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.Last().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.Last().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.Last().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithFilterByCountryUuid_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult1.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName1, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult1.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName1, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult1.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Fact] + public async Task + GetCitiesPage_WithFilterByRegionUuid_WithAdminRole_SearchedCitiesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + string countryName2 = "Country Name 2"; + + var addCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + var addCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName1 = "Region Name 1"; + string regionName2 = "Region Name 2"; + + var addRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName1, + CountryUuid = addCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName2, + CountryUuid = addCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + string cityName1 = "City Name 1"; + string cityName2 = "City Name 2"; + + var addCityResult1 = await mediator.Send( + new AddCityCommand() + { + Name = cityName1, + RegionUuid = addRegionResult1.Uuid + }, TestContext.Current.CancellationToken); + + var addCityResult2 = await mediator.Send( + new AddCityCommand() + { + Name = cityName2, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getCitiesResult = await mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = 1, + PageSize = 10, + RegionUuid = addRegionResult2.Uuid + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCitiesResult); + + Assert.Equal(1, getCitiesResult.PageNumber); + Assert.Equal(1, getCitiesResult.TotalCount); + Assert.Equal(1, getCitiesResult.TotalPages); + Assert.False(getCitiesResult.HasNextPage); + Assert.False(getCitiesResult.HasPreviousPage); + Assert.NotNull(getCitiesResult.Items); + Assert.Single(getCitiesResult.Items); + + + Assert.NotNull(getCitiesResult.Items.First()); + + Assert.NotNull(getCitiesResult.Items.First().Name); + Assert.Equal( + addCityResult2.Name, getCitiesResult.Items.First().Name); + + Assert.NotNull(getCitiesResult.Items.First().CountryName); + Assert.Equal( + countryName2, getCitiesResult.Items.First().CountryName); + Assert.Equal( + addCountryResult2.Uuid, getCitiesResult.Items.First().CountryUuid); + + Assert.NotNull(getCitiesResult.Items.First().RegionName); + Assert.Equal( + regionName2, getCitiesResult.Items.First().RegionName); + Assert.Equal( + addRegionResult2.Uuid, getCitiesResult.Items.First().RegionUuid); + } + + [Theory] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + GetCitiesPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + (string search) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + Search = search + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + public async Task + GetCitiesPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + (int pageNumber) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + PageNumber = pageNumber + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + [InlineData(51)] + [InlineData(int.MaxValue)] + public async Task + GetCitiesPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + (int pageSize) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery() + { + PageSize = pageSize + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCitiesPage_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCitiesPageQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetCitiesPage_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send( + new GetCityQuery(), + TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) +} diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs index 16c42eb..d9ff392 100644 --- a/tst/Application.IntegrationTests/RegionsTests.cs +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -28,7 +28,7 @@ public class RegionsTests : TestBase Name = countryName }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; var createRegionResult = await mediator.Send( new AddRegionCommand() @@ -65,7 +65,7 @@ public class RegionsTests : TestBase Name = countryName }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; var createRegionResult = await mediator.Send( new AddRegionCommand() @@ -106,7 +106,7 @@ public class RegionsTests : TestBase Name = countryName2 }, TestContext.Current.CancellationToken); - string regionName = "Regin Name"; + string regionName = "Region Name"; var createRegionResult1 = await mediator.Send( new AddRegionCommand() @@ -517,8 +517,6 @@ public class RegionsTests : TestBase // TODO: Add more tests with unauthenticated user // (copy tests with admin role) - // TODO: Add test for GetRegion and GetRegionPage - [Fact] public async Task GetRegion_WithAdminRole_RegionReturned() {