From cd279b925f074b60100bee64f3cc91eab8a2e279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D1=82?= =?UTF-8?q?=D0=B5=D0=BF=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Wed, 28 Jun 2023 16:33:27 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0?= =?UTF-8?q?=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Добавил модель данных 2. Добавил Dto для справки 3. Добавил доменный сервис + сделал покрытие тестами 4. Добавил репозиторий для справки 5. Сделал регистрацию зависимостей 6. Добавил контроллер содержащий методы: создания, редактирования, получения файла справки --- AsbCloudApp/Data/HelpPageDto.cs | 33 +++ .../Repositories/IHelpPageRepository.cs | 34 +++ AsbCloudApp/Services/IHelpPageService.cs | 70 +++++++ AsbCloudDb/Model/HelpPage.cs | 30 +++ AsbCloudInfrastructure/DependencyInjection.cs | 7 + .../Repository/HelpPageRepository.cs | 43 ++++ .../Services/HelpPageService.cs | 168 +++++++++++++++ .../ServicesTests/HelpPageServiceTest.cs | 196 ++++++++++++++++++ .../Controllers/HelpPageController.cs | 125 +++++++++++ 9 files changed, 706 insertions(+) create mode 100644 AsbCloudApp/Data/HelpPageDto.cs create mode 100644 AsbCloudApp/Repositories/IHelpPageRepository.cs create mode 100644 AsbCloudApp/Services/IHelpPageService.cs create mode 100644 AsbCloudDb/Model/HelpPage.cs create mode 100644 AsbCloudInfrastructure/Repository/HelpPageRepository.cs create mode 100644 AsbCloudInfrastructure/Services/HelpPageService.cs create mode 100644 AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs create mode 100644 AsbCloudWebApi/Controllers/HelpPageController.cs diff --git a/AsbCloudApp/Data/HelpPageDto.cs b/AsbCloudApp/Data/HelpPageDto.cs new file mode 100644 index 00000000..35f58700 --- /dev/null +++ b/AsbCloudApp/Data/HelpPageDto.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace AsbCloudApp.Data; + +public class HelpPageDto : IId +{ + /// + /// Id записи + /// + public int Id { get; set; } + + /// + /// Id категории файла + /// + [Range(1, int.MaxValue, ErrorMessage = "Id категории файла не может быть меньше 1")] + public int IdCategory { get; set; } + + /// + /// Url страницы, которой пренадлежит справка + /// + public string UrlPage { get; set; } = null!; + + /// + /// Имя файла + /// + [StringLength(260, MinimumLength = 1, ErrorMessage = "Допустимое имя файла от 1 до 260 символов")] + public string Name { get; set; } = null!; + + /// + /// Размер файла + /// + public long Size { get; set; } +} diff --git a/AsbCloudApp/Repositories/IHelpPageRepository.cs b/AsbCloudApp/Repositories/IHelpPageRepository.cs new file mode 100644 index 00000000..aa56a424 --- /dev/null +++ b/AsbCloudApp/Repositories/IHelpPageRepository.cs @@ -0,0 +1,34 @@ +using AsbCloudApp.Data; +using AsbCloudApp.Services; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudApp.Repositories; + +/// +/// Интерфейс репозитория справок страниц +/// +public interface IHelpPageRepository : ICrudRepository +{ + /// + /// Получение справки по url страницы и id категории + /// + /// + /// + /// + /// + Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage, + int idCategory, + CancellationToken cancellationToken); + + /// + /// Проверка на существование справки для определённой страницы в категории + /// + /// + /// + /// + /// + Task IsCheckHelpPageWithUrlPageAndIdCategoryAsync(string urlPage, + int idCategory, + CancellationToken cancellationToken); +} diff --git a/AsbCloudApp/Services/IHelpPageService.cs b/AsbCloudApp/Services/IHelpPageService.cs new file mode 100644 index 00000000..726f4566 --- /dev/null +++ b/AsbCloudApp/Services/IHelpPageService.cs @@ -0,0 +1,70 @@ +using AsbCloudApp.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudApp.Services; + +/// +/// Интерфейс сервиса справок страниц +/// +public interface IHelpPageService +{ + /// + /// Создание справки + /// + /// + /// + /// + /// + /// + /// + Task CreateAsync(string urlPage, + int idCategory, + string fileName, + Stream fileStream, + CancellationToken cancellationToken); + + /// + /// Редактирование справки + /// + /// + /// + /// + /// + /// + /// + Task UpdateAsync(HelpPageDto helpPage, + int idCategory, + string fileName, + Stream fileStream, + CancellationToken cancellationToken); + + /// + /// Получение справки по url страницы и id категории + /// + /// + /// + /// + /// + Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage, + int idCategory, + CancellationToken cancellationToken); + + /// + /// Получение справки по Id + /// + /// + /// + /// + Task GetOrDefaultByIdAsync(int id, + CancellationToken cancellationToken); + + /// + /// Получение файла справки по Id + /// + /// + /// + /// + Stream GetFileStream(HelpPageDto helpPage); +} \ No newline at end of file diff --git a/AsbCloudDb/Model/HelpPage.cs b/AsbCloudDb/Model/HelpPage.cs new file mode 100644 index 00000000..fe84254f --- /dev/null +++ b/AsbCloudDb/Model/HelpPage.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace AsbCloudDb.Model; + +[Table("t_help_page"), Comment("Справки")] +public class HelpPage : IId +{ + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("url_page"), Comment("Url страницы")] + public string UrlPage { get; set; } = null!; + + [Column("id_category"), Comment("Id категории файла")] + public int IdCategory { get; set; } + + [Column("name"), Comment("Название файла")] + public string Name { get; set; } = null!; + + [Column("file_size"), Comment("Размер файла")] + public long Size { get; set; } + + [JsonIgnore] + [ForeignKey(nameof(IdCategory))] + public virtual FileCategory FileCategory { get; set; } = null!; +} diff --git a/AsbCloudInfrastructure/DependencyInjection.cs b/AsbCloudInfrastructure/DependencyInjection.cs index aa4734bd..c3725e5f 100644 --- a/AsbCloudInfrastructure/DependencyInjection.cs +++ b/AsbCloudInfrastructure/DependencyInjection.cs @@ -136,6 +136,12 @@ namespace AsbCloudInfrastructure services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(serviceProvider => + new HelpPageService( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + configuration.GetSection("HelpPageOptions:DirectoryNameHelpPageFiles") + .Get())); services.AddTransient(); @@ -169,6 +175,7 @@ namespace AsbCloudInfrastructure services.AddTransient, CrudCacheRepositoryBase>(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/AsbCloudInfrastructure/Repository/HelpPageRepository.cs b/AsbCloudInfrastructure/Repository/HelpPageRepository.cs new file mode 100644 index 00000000..443b56db --- /dev/null +++ b/AsbCloudInfrastructure/Repository/HelpPageRepository.cs @@ -0,0 +1,43 @@ +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using AsbCloudDb.Model; +using Mapster; +using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Threading.Tasks; + +namespace AsbCloudInfrastructure.Repository; + +public class HelpPageRepository : CrudRepositoryBase, + IHelpPageRepository +{ + public HelpPageRepository(IAsbCloudDbContext context) + : base(context) + { + } + + public async Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage, + int idCategory, + CancellationToken cancellationToken) + { + var helpPage = await dbSet.AsNoTracking() + .SingleOrDefaultAsync(x => + x.UrlPage == urlPage && + x.IdCategory == idCategory, + cancellationToken); + + if (helpPage is null) + return null; + + return helpPage.Adapt(); + } + + public Task IsCheckHelpPageWithUrlPageAndIdCategoryAsync(string urlPage, int idCategory, + CancellationToken cancellationToken) + { + return dbSet.AnyAsync(x => + x.UrlPage == urlPage && + x.IdCategory == idCategory, + cancellationToken); + } +} diff --git a/AsbCloudInfrastructure/Services/HelpPageService.cs b/AsbCloudInfrastructure/Services/HelpPageService.cs new file mode 100644 index 00000000..7be9e065 --- /dev/null +++ b/AsbCloudInfrastructure/Services/HelpPageService.cs @@ -0,0 +1,168 @@ +using System; +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Exceptions; + +namespace AsbCloudInfrastructure.Services; + +/// +/// Реализация сервиса справок страниц +/// +public class HelpPageService : IHelpPageService +{ + private readonly string directoryNameHelpPageFiles; + private readonly IHelpPageRepository helpPageRepository; + private readonly IFileStorageRepository fileStorageRepository; + + /// + /// Конструктор класса + /// + /// + /// + /// + public HelpPageService(IHelpPageRepository helpPageRepository, + IFileStorageRepository fileStorageRepository, + string directoryNameHelpPageFiles) + { + if (string.IsNullOrWhiteSpace(directoryNameHelpPageFiles)) + throw new ArgumentException("Value cannot be null or whitespace", nameof(this.directoryNameHelpPageFiles)); + + this.helpPageRepository = helpPageRepository; + this.fileStorageRepository = fileStorageRepository; + this.directoryNameHelpPageFiles = directoryNameHelpPageFiles; + } + + /// + /// Создание справки страницы + /// + /// + /// + /// + /// + /// + /// + public async Task CreateAsync(string urlPage, + int idCategory, + string fileName, + Stream fileStream, + CancellationToken cancellationToken) + { + if (await helpPageRepository.IsCheckHelpPageWithUrlPageAndIdCategoryAsync(urlPage, + idCategory, + cancellationToken)) + { + throw new ArgumentsInvalidException("Справка с такой категории файла для данной страницы уже существует", + new[] { nameof(urlPage), nameof(idCategory) }); + } + + HelpPageDto helpPage = new() + { + UrlPage = urlPage, + IdCategory = idCategory, + Name = Path.GetFileName(fileName), + Size = fileStream.Length, + }; + + int idFile = await helpPageRepository.InsertAsync(helpPage, + cancellationToken); + + await SaveFileAsync(idCategory, + fileName, + fileStream, + idFile, + cancellationToken); + + return idFile; + } + + /// + /// Обновление справки страницы + /// + /// + /// + /// + /// + /// + /// + public async Task UpdateAsync(HelpPageDto helpPage, + int idCategory, + string fileName, + Stream fileStream, + CancellationToken cancellationToken) + { + helpPage.Name = Path.GetFileName(fileName); + helpPage.IdCategory = idCategory; + helpPage.Size = fileStream.Length; + + string fileFullName = fileStorageRepository.GetFilePath(directoryNameHelpPageFiles, + idCategory.ToString(), + helpPage.Id, + Path.GetExtension(helpPage.Name)); + + await helpPageRepository.UpdateAsync(helpPage, + cancellationToken); + + fileStorageRepository.DeleteFile(fileFullName); + + await SaveFileAsync(helpPage.IdCategory, + fileName, + fileStream, + helpPage.Id, + cancellationToken); + } + + /// + /// Получение справки по url страницы и id категории + /// + /// + /// + /// + /// + public Task GetOrDefaultByUrlPageAndIdCategoryAsync(string urlPage, + int idCategory, + CancellationToken cancellationToken) => + helpPageRepository.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage, + idCategory, + cancellationToken); + + public Task GetOrDefaultByIdAsync(int id, CancellationToken cancellationToken) => + helpPageRepository.GetOrDefaultAsync(id, + cancellationToken); + + /// + /// Получение файлового потока для файла справки + /// + /// + /// + /// + public Stream GetFileStream(HelpPageDto helpPage) + { + string filePath = fileStorageRepository.GetFilePath(directoryNameHelpPageFiles, + helpPage.IdCategory.ToString(), + helpPage.Id, + Path.GetExtension(helpPage.Name));; + + var fileStream = new FileStream(Path.GetFullPath(filePath), FileMode.Open); + + return fileStream; + } + + private async Task SaveFileAsync(int idCategory, + string fileName, + Stream fileStream, + int fileId, + CancellationToken cancellationToken = default) + { + string filePath = fileStorageRepository.MakeFilePath(directoryNameHelpPageFiles, + idCategory.ToString(), + $"{fileId}" + $"{Path.GetExtension(fileName)}"); + + await fileStorageRepository.SaveFileAsync(filePath, + fileStream, + cancellationToken); + } +} diff --git a/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs new file mode 100644 index 00000000..83fcf51c --- /dev/null +++ b/AsbCloudWebApi.Tests/ServicesTests/HelpPageServiceTest.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Exceptions; +using AsbCloudApp.Repositories; +using AsbCloudApp.Services; +using AsbCloudInfrastructure.Services; +using Moq; +using Xunit; + +namespace AsbCloudWebApi.Tests.ServicesTests; + +public class HelpPageServiceTest +{ + private const string directoryNameHelpPageFiles = "helpPages"; + + private static List HelpPages = new() + { + new() + { + Id = 123, + IdCategory = 20000, + Name = "1.pdf", + Size = 54000, + UrlPage = "test" + }, + new() + { + Id = 134, + IdCategory = 20000, + Name = "2.pdf", + Size = 51000, + UrlPage = "test1" + }, + new() + { + Id = 178, + IdCategory = 10000, + Name = "3.pdf", + Size = 49000, + UrlPage = "test2" + } + }; + + private readonly Mock helpPageRepository = new(); + private readonly Mock fileStorageRepository = new(); + + private readonly IHelpPageService helpPageService; + + public HelpPageServiceTest() + { + helpPageService = new HelpPageService(helpPageRepository.Object, + fileStorageRepository.Object, + directoryNameHelpPageFiles); + } + + [Fact] + public async Task CreateAsync_ShouldReturn_PositiveId() + { + //arrange + int idHelpPage = new Random().Next(1, 100); + string urlPage = "test"; + int idCategory = 20000; + string fullName = "test.pdf"; + MemoryStream fileStream = new MemoryStream(Array.Empty()); + + helpPageRepository.Setup(x => x.InsertAsync(It.IsAny(), + It.IsAny())) + .Returns(() => Task.FromResult(idHelpPage)); + + fileStorageRepository.Setup(x => x.SaveFileAsync(It.IsAny(), + It.IsAny(), + It.IsAny())); + + //act + int result = await helpPageService.CreateAsync(urlPage, + idCategory, + fullName, + fileStream, + CancellationToken.None); + + //assert + Assert.True(result > 0); + } + + [Fact] + public async Task CreateAsync_ShouldReturn_ArgumentsInvalidException() + { + //arrange + string urlPage = "test"; + int idCategory = 20000; + string fullName = "test.pdf"; + MemoryStream fileStream = new MemoryStream(Array.Empty()); + bool isExistingHelpPage = true; + + helpPageRepository.Setup(x => x.IsCheckHelpPageWithUrlPageAndIdCategoryAsync(It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(() => Task.FromResult(isExistingHelpPage)); + + //act + Task Result () => helpPageService.CreateAsync(urlPage, + idCategory, + fullName, + fileStream, + CancellationToken.None); + + //assert + await Assert.ThrowsAsync(Result); + } + + [Fact] + public async Task UpdateAsync_ShouldReturn_UpdatedHelpPage() + { + //arrange + HelpPageDto helpPage = new() + { + Id = 123, + IdCategory = 134, + UrlPage = "test", + Name = ".pdf", + Size = 54000 + }; + + int newIdCategory = 451; + string newFileName = " .pdf"; + MemoryStream newFileStream = new MemoryStream(Array.Empty()); + + //act + await helpPageService.UpdateAsync(helpPage, + newIdCategory, + newFileName, + newFileStream, + CancellationToken.None); + + //assert + Assert.Equal(newFileName, helpPage.Name); + Assert.Equal(newIdCategory, helpPage.IdCategory); + Assert.Equal(newFileStream.Length, helpPage.Size); + } + + [Theory] + [InlineData(20000, "test")] + [InlineData(20000, "test1")] + public async Task GetOrDefaultByUrlPageAndIdCategoryAsync_ShouldReturn_HelpPageDto(int idCategory, + string urlPage) + { + //arrange + helpPageRepository.Setup(x => x.GetOrDefaultByUrlPageAndIdCategoryAsync(It.IsAny(), + It.IsAny(), It.IsAny())) + .Returns(() => + { + var helpPage = HelpPages.FirstOrDefault(x => + x.UrlPage == urlPage && + x.IdCategory == idCategory); + + return Task.FromResult(helpPage); + }); + + //act + var result = await helpPageService.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage, + idCategory, + CancellationToken.None); + + //assert + Assert.NotNull(result); + } + + [Theory] + [InlineData(123)] + [InlineData(178)] + public async Task GetOrDefaultByIdAsync_ShouldReturn_HelpPageDto(int id) + { + //arrange + helpPageRepository.Setup(x => x.GetOrDefaultAsync(It.IsAny(), + It.IsAny())) + .Returns(() => + { + var helpPage = HelpPages.FirstOrDefault(x => + x.Id == id); + + return Task.FromResult(helpPage); + }); + + //act + var result = await helpPageService.GetOrDefaultByIdAsync(id, + CancellationToken.None); + + //assert + Assert.NotNull(result); + } +} \ No newline at end of file diff --git a/AsbCloudWebApi/Controllers/HelpPageController.cs b/AsbCloudWebApi/Controllers/HelpPageController.cs new file mode 100644 index 00000000..d3bc0a48 --- /dev/null +++ b/AsbCloudWebApi/Controllers/HelpPageController.cs @@ -0,0 +1,125 @@ +using AsbCloudApp.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using AsbCloudApp.Data; +using AsbCloudApp.Repositories; +using Microsoft.AspNetCore.Authorization; + +namespace AsbCloudWebApi.Controllers; + +/// +/// Справки по страницам +/// +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class HelpPageController : ControllerBase +{ + private readonly IHelpPageService helpPageService; + private readonly IUserRepository userRepository; + + public HelpPageController(IHelpPageService helpPageService, + IUserRepository userRepository) + { + this.helpPageService = helpPageService; + this.userRepository = userRepository; + } + + /// + /// Создание файла справки + /// + /// Url страницы для которой предназначена эта справка + /// Id катагории файла + /// Загружаемый файл + /// Id созданной справки + [HttpPost] + [Permission] + [Route("saveFile")] + [ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)] + public async Task CreateAsync(string urlPage, + int idCategory, + [Required] IFormFile file) + { + int? idUser = User.GetUserId(); + + if(!idUser.HasValue) + return Forbid(); + + if (!userRepository.HasPermission(idUser.Value, $"HelpPage.create")) + return Forbid(); + + var helpPage = await helpPageService.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage, + idCategory, + CancellationToken.None); + + using var fileStream = file.OpenReadStream(); + + if (helpPage is not null) + { + await helpPageService.UpdateAsync(helpPage, + idCategory, + file.FileName, + fileStream, + CancellationToken.None); + + return Ok(helpPage.Id); + } + + int helpPageId = await helpPageService.CreateAsync(urlPage, + idCategory, + file.FileName, + fileStream, + CancellationToken.None); + + return Ok(helpPageId); + } + + /// + /// Получение файла справки + /// + /// Id справки + /// Файл + [HttpGet] + [Route("getById/{id}")] + [ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)] + public async Task GetFileAsync(int id) + { + var helpPage = await helpPageService.GetOrDefaultByIdAsync(id, + CancellationToken.None); + + if (helpPage is null) + return NotFound(); + + using var fileStream = helpPageService.GetFileStream(helpPage); + + var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, + CancellationToken.None); + memoryStream.Position = 0; + + return File(memoryStream, "application/octet-stream", helpPage.Name); + } + + /// + /// Получение информации о справке + /// + /// Url страницы + /// Id категории + /// Dto справки + [HttpGet] + [Route("getByUrlPage/{urlPage}/{idCategory}")] + [ProducesResponseType(typeof(HelpPageDto), (int)System.Net.HttpStatusCode.OK)] + public async Task GetByUrlPageAsync(string urlPage, + int idCategory) + { + var helpPage = await helpPageService.GetOrDefaultByUrlPageAndIdCategoryAsync(urlPage, + idCategory, + CancellationToken.None); + + return Ok(helpPage); + } +}