diff --git a/Streetcode/Streetcode.BLL/DTO/Media/Video/CreateVideoDTO.cs b/Streetcode/Streetcode.BLL/DTO/Media/Video/CreateVideoDTO.cs new file mode 100644 index 000000000..d4d903d37 --- /dev/null +++ b/Streetcode/Streetcode.BLL/DTO/Media/Video/CreateVideoDTO.cs @@ -0,0 +1,6 @@ +namespace Streetcode.BLL.DTO.Media.Video +{ + public class CreateVideoDTO : VideoDTO + { + } +} diff --git a/Streetcode/Streetcode.BLL/DTO/Media/Video/VideoDTO.cs b/Streetcode/Streetcode.BLL/DTO/Media/Video/VideoDTO.cs index 28900e1d4..b49d6c42d 100644 --- a/Streetcode/Streetcode.BLL/DTO/Media/Video/VideoDTO.cs +++ b/Streetcode/Streetcode.BLL/DTO/Media/Video/VideoDTO.cs @@ -1,11 +1,10 @@ -using Streetcode.BLL.DTO.AdditionalContent; - namespace Streetcode.BLL.DTO.Media.Video; public class VideoDTO { - public int Id { get; set; } - public string? Description { get; set; } - public string? Url { get; set; } - public int StreetcodeId { get; set; } -} + public int Id { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public string? Url { get; set; } + public int StreetcodeId { get; set; } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/DTO/Media/VideoDTO.cs b/Streetcode/Streetcode.BLL/DTO/Media/VideoDTO.cs deleted file mode 100644 index 7d53055a4..000000000 --- a/Streetcode/Streetcode.BLL/DTO/Media/VideoDTO.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Streetcode.BLL.DTO.AdditionalContent; - -namespace Streetcode.BLL.DTO.Media; - -public class VideoDTO -{ - public int Id { get; set; } - public string? Description { get; set; } - public string? Url { get; set; } - public int StreetcodeId { get; set; } -} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/DTO/TextContent/TermCreateDTO.cs b/Streetcode/Streetcode.BLL/DTO/TextContent/TermCreateDTO.cs new file mode 100644 index 000000000..8b1c35e00 --- /dev/null +++ b/Streetcode/Streetcode.BLL/DTO/TextContent/TermCreateDTO.cs @@ -0,0 +1,10 @@ +namespace Streetcode.BLL.DTO.Streetcode.TextContent; + +public class TermCreateDTO +{ + public string Title { get; set; } + + public string Description { get; set; } + + public int StreetcodeId { get; set; } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Mapping/Streetcode/TextContent/TermProfile.cs b/Streetcode/Streetcode.BLL/Mapping/Streetcode/TextContent/TermProfile.cs index f3707b07a..50902d385 100644 --- a/Streetcode/Streetcode.BLL/Mapping/Streetcode/TextContent/TermProfile.cs +++ b/Streetcode/Streetcode.BLL/Mapping/Streetcode/TextContent/TermProfile.cs @@ -9,5 +9,6 @@ public class TermProfile : Profile public TermProfile() { CreateMap().ReverseMap(); + CreateMap(); } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoCommand.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoCommand.cs new file mode 100644 index 000000000..eb461b9b5 --- /dev/null +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoCommand.cs @@ -0,0 +1,7 @@ +using FluentResults; +using MediatR; +using Streetcode.BLL.DTO.Media.Video; + +namespace Streetcode.BLL.MediatR.Media.Video.Create; + +public record CreateVideoCommand(CreateVideoDTO CreateVideoRequest) : IRequest>; \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoHandler.cs new file mode 100644 index 000000000..e538ac0a3 --- /dev/null +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoHandler.cs @@ -0,0 +1,80 @@ +using AutoMapper; +using FluentResults; +using MediatR; +using Microsoft.IdentityModel.Tokens; +using Streetcode.BLL.DTO.Media.Video; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.DAL.Repositories.Interfaces.Base; + +using Entity = Streetcode.DAL.Entities.Media.Video; + +namespace Streetcode.BLL.MediatR.Media.Video.Create; + +public class CreateVideoHandler : IRequestHandler> +{ + private readonly IMapper _mapper; + private readonly IRepositoryWrapper _repositoryWrapper; + private readonly ILoggerService _logger; + + public CreateVideoHandler( + IRepositoryWrapper repositoryWrapper, + IMapper mapper, + ILoggerService logger) + { + _repositoryWrapper = repositoryWrapper; + _mapper = mapper; + _logger = logger; + } + + public async Task> Handle(CreateVideoCommand request, CancellationToken cancellationToken) + { + var newVideo = _mapper.Map(request.CreateVideoRequest); + + var validation = VideoValidation(request, newVideo); + if (validation != null) + { + return validation; + } + + var entity = await _repositoryWrapper.VideoRepository.CreateAsync(newVideo); + var resultIsSuccess = await _repositoryWrapper.SaveChangesAsync() > 0; + if (resultIsSuccess) + { + return Result.Ok(_mapper.Map(entity)); + } + else + { + const string errorMsg = "Failed to create a Video."; + return LogAndFail(request, errorMsg); + } + } + + private Result? VideoValidation(CreateVideoCommand request, Entity? newVideo) + { + if (newVideo == null) + { + const string errorMsg = "Cannot convert null to Video."; + return LogAndFail(request, errorMsg); + } + + if (newVideo.Title.Length > 100) + { + const string errorMsg = "Çàãîëîâîê â³äåî íå ìîæå áóòè á³ëüøå 100 ñèìâîë³â."; + return LogAndFail(request, errorMsg); + } + + if (newVideo.Url.IsNullOrEmpty()) + { + const string errorMsg = "Ïîñèëàííÿ íà â³äåî º îáîâ'ÿçêîâèì."; + return LogAndFail(request, errorMsg); + } + + return null; + } + + private Result LogAndFail(CreateVideoCommand request, string errorMsg) + { + _logger.LogError(request, errorMsg); + return Result.Fail(errorMsg); + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/Create/CreateTermCommand.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/Create/CreateTermCommand.cs new file mode 100644 index 000000000..82a2c1569 --- /dev/null +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/Create/CreateTermCommand.cs @@ -0,0 +1,7 @@ +using FluentResults; +using MediatR; +using Streetcode.BLL.DTO.Streetcode.TextContent; + +namespace Streetcode.BLL.MediatR.Streetcode.Term.Create; + +public record CreateTermCommand(TermCreateDTO Term) : IRequest>; \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/Create/CreateTermHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/Create/CreateTermHandler.cs new file mode 100644 index 000000000..d56c2eb10 --- /dev/null +++ b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/Create/CreateTermHandler.cs @@ -0,0 +1,49 @@ +using AutoMapper; +using FluentResults; +using MediatR; +using Streetcode.BLL.DTO.Streetcode.TextContent; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.DAL.Repositories.Interfaces.Base; + +using TermEntity = Streetcode.DAL.Entities.Streetcode.TextContent.Term; + +namespace Streetcode.BLL.MediatR.Streetcode.Term.Create; + +public class CreateTermHandler : IRequestHandler> +{ + private readonly IRepositoryWrapper _repository; + private readonly IMapper _mapper; + private readonly ILoggerService _logger; + + public CreateTermHandler(IRepositoryWrapper repository, IMapper mapper, ILoggerService logger) + { + _repository = repository; + _mapper = mapper; + _logger = logger; + } + + public async Task> Handle(CreateTermCommand request, CancellationToken cancellationToken) + { + var entity = _mapper.Map(request.Term); + + if (entity is null) + { + const string errorMsg = "Cannot map CreateTermRequest to entity."; + _logger.LogError(request, errorMsg); + return Result.Fail(errorMsg); + } + + await _repository.TermRepository.CreateAsync(entity); + var result = await _repository.SaveChangesAsync(); + + if (result > 0) + { + var dto = _mapper.Map(entity); + return Result.Ok(dto); + } + + const string failMsg = "Failed to save new Term."; + _logger.LogError(request, failMsg); + return Result.Fail(failMsg); + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextsHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextHandler.cs similarity index 100% rename from Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextsHandler.cs rename to Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextHandler.cs diff --git a/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextsQuery.cs b/Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextQuery.cs similarity index 100% rename from Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextsQuery.cs rename to Streetcode/Streetcode.BLL/MediatR/Streetcode/Text/GetAll/GetAllTextQuery.cs diff --git a/Streetcode/Streetcode.BLL/Validators/Media/Video/CreateVideoCommandValidator.cs b/Streetcode/Streetcode.BLL/Validators/Media/Video/CreateVideoCommandValidator.cs new file mode 100644 index 000000000..11ef079e5 --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/Media/Video/CreateVideoCommandValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using Streetcode.BLL.MediatR.Media.Video.Create; + +namespace Streetcode.BLL.Validator.Media.Video; + +public sealed class CreateVideoCommandValidator : AbstractValidator +{ + public CreateVideoCommandValidator() + { + RuleFor(c => c.CreateVideoRequest.Title) + .NotEmpty() + .MaximumLength(100); + + RuleFor(c => c.CreateVideoRequest.Url) + .NotEmpty() + .Must(url => Uri.IsWellFormedUriString(url, UriKind.Absolute)); + } +} diff --git a/Streetcode/Streetcode.BLL/Validators/TextContent/CreateTermValidator.cs b/Streetcode/Streetcode.BLL/Validators/TextContent/CreateTermValidator.cs new file mode 100644 index 000000000..cbb2af623 --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/TextContent/CreateTermValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using Streetcode.BLL.MediatR.Streetcode.Term.Create; + +namespace Streetcode.BLL.Validator.Streetcode.Term.Create; + +public sealed class CreateTermValidator : AbstractValidator +{ + public CreateTermValidator() + { + RuleFor(cmd => cmd.Term.Title) + .NotEmpty() + .MaximumLength(200); + + RuleFor(cmd => cmd.Term.Description) + .NotEmpty(); + + RuleFor(cmd => cmd.Term.StreetcodeId) + .GreaterThan(0); + } +} diff --git a/Streetcode/Streetcode.DAL/Entities/Media/Video.cs b/Streetcode/Streetcode.DAL/Entities/Media/Video.cs index 73cb953de..2ab84c509 100644 --- a/Streetcode/Streetcode.DAL/Entities/Media/Video.cs +++ b/Streetcode/Streetcode.DAL/Entities/Media/Video.cs @@ -16,7 +16,7 @@ public class Video public string? Description { get; set; } [Required] - public string? Url { get; set; } + public string Url { get; set; } [Required] public int StreetcodeId { get; set; } diff --git a/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/RelatedTerm.cs b/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/RelatedTerm.cs index ce1cf42d3..b9b7fe716 100644 --- a/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/RelatedTerm.cs +++ b/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/RelatedTerm.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; namespace Streetcode.DAL.Entities.Streetcode.TextContent @@ -17,4 +17,4 @@ public class RelatedTerm public int TermId { get; set; } public Term? Term { get; set; } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs b/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs index cd24b9e2b..3276bce6f 100644 --- a/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs +++ b/Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs @@ -1,22 +1,28 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Streetcode.DAL.Entities.Streetcode.TextContent; - -[Table("terms", Schema = "streetcode")] -public class Term +namespace Streetcode.DAL.Entities.Streetcode.TextContent { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + [Table("terms", Schema = "streetcode")] + public class Term + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Required] + [MaxLength(50)] + public string Title { get; set; } = string.Empty; - [Required] - [MaxLength(50)] - public string? Title { get; set; } + [Required] + [MaxLength(500)] + public string Description { get; set; } = string.Empty; - [Required] - [MaxLength(500)] - public string? Description { get; set; } + [ForeignKey(nameof(Streetcode))] + public int StreetcodeId { get; set; } - public List RelatedTerms { get; set; } = new(); -} \ No newline at end of file + public ICollection RelatedTerms { get; set; } = new List(); + } +} diff --git a/Streetcode/Streetcode.WebApi/Controllers/Media/VideoController.cs b/Streetcode/Streetcode.WebApi/Controllers/Media/VideoController.cs index 451bc0051..a78dff636 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Media/VideoController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Media/VideoController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; -using Streetcode.BLL.DTO.Media; +using Streetcode.BLL.DTO.Media.Video; +using Streetcode.BLL.MediatR.Media.Video.Create; using Streetcode.BLL.MediatR.Media.Video.GetAll; using Streetcode.BLL.MediatR.Media.Video.GetById; using Streetcode.BLL.MediatR.Media.Video.GetByStreetcodeId; @@ -25,4 +26,10 @@ public async Task GetById([FromRoute] int id) { return HandleResult(await Mediator.Send(new GetVideoByIdQuery(id))); } -} + + [HttpPost] + public async Task Create([FromBody] CreateVideoDTO video) + { + return HandleResult(await Mediator.Send(new CreateVideoCommand(video))); + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj b/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj index 043e57173..0a4fa244f 100644 --- a/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj +++ b/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj @@ -14,6 +14,7 @@ + diff --git a/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Media/Video/CreateVideoHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Media/Video/CreateVideoHandlerTests.cs new file mode 100644 index 000000000..ddff736aa --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Media/Video/CreateVideoHandlerTests.cs @@ -0,0 +1,156 @@ +using System.Linq.Expressions; +using AutoMapper; +using FluentAssertions; +using Moq; +using Streetcode.BLL.DTO.Media.Video; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.BLL.MediatR.Media.Video.Create; +using Streetcode.DAL.Repositories.Interfaces.Base; +using Xunit; + +using Entity = Streetcode.DAL.Entities.Media.Video; + +namespace Streetcode.XUnitTest.BLL.MediatRTests.Media.Video.Create; + +public class CreateVideoHandlerTests +{ + private readonly Mock _repositoryWrapperMock; + private readonly Mock _mapperMock; + private readonly Mock _loggerMock; + private readonly CreateVideoHandler _handler; + + public CreateVideoHandlerTests() + { + _repositoryWrapperMock = new Mock(); + _mapperMock = new Mock(); + _loggerMock = new Mock(); + + _handler = new CreateVideoHandler( + _repositoryWrapperMock.Object, + _mapperMock.Object, + _loggerMock.Object); + } + + [Fact] + public async Task Handle_OnVideoCreation_ReturnsSuccess() + { + var requestDto = new CreateVideoDTO + { + Title = "Title", + Description = "Description", + Url = "https://google.com", + StreetcodeId = 1 + }; + + var mappedEntity = new Entity + { + Title = "Title", + Description = "Description", + Url = "https://google.com", + StreetcodeId = 1 + }; + + _mapperMock.Setup(m => m.Map(requestDto)).Returns(mappedEntity); + + _repositoryWrapperMock + .Setup(r => r.VideoRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), default)) + .ReturnsAsync((Entity)null); + + _repositoryWrapperMock.Setup(r => r.VideoRepository.CreateAsync(mappedEntity)).ReturnsAsync(mappedEntity); + _repositoryWrapperMock.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1); + + var result = await _handler.Handle(new CreateVideoCommand(requestDto), CancellationToken.None); + + _repositoryWrapperMock.Verify(x => x.VideoRepository.CreateAsync(mappedEntity), Times.Once); + _repositoryWrapperMock.Verify(x => x.SaveChangesAsync(), Times.Once); + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task Handle_EmptyVideo_ReturnsError() + { + CreateVideoDTO requestDto = null; + Entity mappedEntity = null; + + _mapperMock.Setup(m => m.Map(null)).Returns(mappedEntity); + + _repositoryWrapperMock + .Setup(r => r.VideoRepository.GetFirstOrDefaultAsync(It.IsAny>>(), default)) + .ReturnsAsync((Entity)null); + + _repositoryWrapperMock.Setup(r => r.VideoRepository.CreateAsync(mappedEntity)).ReturnsAsync(mappedEntity); + _repositoryWrapperMock.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1); + + var result = await _handler.Handle(new CreateVideoCommand(requestDto), CancellationToken.None); + + result.IsFailed.Should().BeTrue(); + result.Errors.First().Message.Should().Contain("Cannot convert null to Video."); + } + + [Fact] + public async Task Handle_TitleMaxLength_ReturnsError() + { + var requestDto = new CreateVideoDTO + { + Title = new string('*', 101), + Description = "Description", + Url = "https://google.com", + StreetcodeId = 1 + }; + + var mappedEntity = new Entity + { + Title = new string('*', 101), + Description = "Description", + Url = "https://google.com", + StreetcodeId = 1 + }; + + _mapperMock.Setup(m => m.Map(requestDto)).Returns(mappedEntity); + + _repositoryWrapperMock + .Setup(r => r.VideoRepository.GetFirstOrDefaultAsync(It.IsAny>>(), default)) + .ReturnsAsync((Entity)null); + + _repositoryWrapperMock.Setup(r => r.VideoRepository.CreateAsync(mappedEntity)).ReturnsAsync(mappedEntity); + _repositoryWrapperMock.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1); + + var result = await _handler.Handle(new CreateVideoCommand(requestDto), CancellationToken.None); + + result.IsFailed.Should().BeTrue(); + result.Errors.First().Message.Should().Contain("Çàãîëîâîê â³äåî íå ìîæå áóòè á³ëüøå 100 ñèìâîë³â."); + } + + [Fact] + public async Task Handle_EmptyUrl_ReturnsError() + { + var requestDto = new CreateVideoDTO + { + Title = "Title", + Description = "Description", + StreetcodeId = 1 + }; + + var mappedEntity = new Entity + { + Title = "Title", + Description = "Description", + StreetcodeId = 1 + }; + + _mapperMock.Setup(m => m.Map(requestDto)).Returns(mappedEntity); + + _repositoryWrapperMock + .Setup(r => r.VideoRepository.GetFirstOrDefaultAsync(It.IsAny>>(), default)) + .ReturnsAsync((Entity)null); + + _repositoryWrapperMock.Setup(r => r.VideoRepository.CreateAsync(mappedEntity)).ReturnsAsync(mappedEntity); + _repositoryWrapperMock.Setup(r => r.SaveChangesAsync()).ReturnsAsync(1); + + var result = await _handler.Handle(new CreateVideoCommand(requestDto), CancellationToken.None); + + result.IsFailed.Should().BeTrue(); + result.Errors.First().Message.Should().Contain("Ïîñèëàííÿ íà â³äåî º îáîâ'ÿçêîâèì."); + } +} diff --git a/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj b/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj index f071cb0fa..ce5e6440b 100644 --- a/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj +++ b/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj @@ -8,6 +8,8 @@ + +