diff --git a/Streetcode/Streetcode.BLL/Interfaces/BlobStorage/IBlobService.cs b/Streetcode/Streetcode.BLL/Interfaces/BlobStorage/IBlobService.cs index eace1bc..bcbbec1 100644 --- a/Streetcode/Streetcode.BLL/Interfaces/BlobStorage/IBlobService.cs +++ b/Streetcode/Streetcode.BLL/Interfaces/BlobStorage/IBlobService.cs @@ -2,13 +2,14 @@ public interface IBlobService { - public string SaveFileInStorage(string base64, string name, string mimeType); - public MemoryStream FindFileInStorageAsMemoryStream(string name); - public string UpdateFileInStorage( + public Task SaveFileInStorage(string base64, string name, string mimeType); + public Task UpdateFileInStorage( string previousBlobName, string base64Format, string newBlobName, string extension); - public string FindFileInStorageAsBase64(string name); - public void DeleteFileInStorage(string name); + public Task FindFileInStorageAsMemoryStream(string name); + public Task FindFileInStorageAsBase64(string name); + public Task DeleteFileInStorage(string name); + public Task CleanBlobStorage(); } diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Art/GetByStreetcodeId/GetArtsByStreetcodeIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Art/GetByStreetcodeId/GetArtsByStreetcodeIdHandler.cs index b460ea1..f3cb9f7 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Art/GetByStreetcodeId/GetArtsByStreetcodeIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Art/GetByStreetcodeId/GetArtsByStreetcodeIdHandler.cs @@ -52,10 +52,24 @@ public async Task>> Handle(GetArtsByStreetcodeIdQuery foreach (var artDto in artsDto) { - if (artDto.Image != null && artDto.Image.BlobName != null) + if (artDto.Image?.BlobName == null) { - artDto.Image.Base64 = _blobService.FindFileInStorageAsBase64(artDto.Image.BlobName); + continue; } + + var imageBase64 = await _blobService.FindFileInStorageAsBase64(artDto.Image.BlobName); + if (imageBase64 is not null) + { + artDto.Image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + artDto.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(artsDto); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/Create/CreateAudioHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/Create/CreateAudioHandler.cs index 6b15ec6..b9de4f7 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/Create/CreateAudioHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/Create/CreateAudioHandler.cs @@ -32,7 +32,7 @@ public CreateAudioHandler( public async Task> Handle(CreateAudioCommand request, CancellationToken cancellationToken) { - var hashBlobStorageName = _blobService.SaveFileInStorage( + var hashBlobStorageName = await _blobService.SaveFileInStorage( request.Audio.BaseFormat, request.Audio.Title, request.Audio.Extension); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetAll/GetAllAudiosHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetAll/GetAllAudiosHandler.cs index 1963dfb..72b07c6 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetAll/GetAllAudiosHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetAll/GetAllAudiosHandler.cs @@ -40,7 +40,19 @@ public async Task>> Handle(GetAllAudiosQuery reques var audioDtos = _mapper.Map>(audios); foreach (var audio in audioDtos) { - audio.Base64 = _blobService.FindFileInStorageAsBase64(audio.BlobName); + var audioBase64 = await _blobService.FindFileInStorageAsBase64(audio.BlobName); + if (audioBase64 is not null) + { + audio.Base64 = audioBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Audio), + audio.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(audioDtos); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetBaseAudio/GetBaseAudioHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetBaseAudio/GetBaseAudioHandler.cs index ce7404d..8202fde 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetBaseAudio/GetBaseAudioHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetBaseAudio/GetBaseAudioHandler.cs @@ -28,7 +28,18 @@ public async Task> Handle(GetBaseAudioQuery request, Cancel if (audio is not null) { - return _blobStorage.FindFileInStorageAsMemoryStream(audio.BlobName); + var audioMemoryStream = await _blobStorage.FindFileInStorageAsMemoryStream(audio.BlobName); + if (audioMemoryStream is not null) + { + return Result.Ok(audioMemoryStream); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Audio), + audio.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } var errorMsg = Messages.Error_EntityWithIdNotFound.Format(nameof(AudioEntity), request.Id); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetById/GetAudioByIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetById/GetAudioByIdHandler.cs index f39eed4..2f483ad 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetById/GetAudioByIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetById/GetAudioByIdHandler.cs @@ -39,8 +39,19 @@ public async Task> Handle(GetAudioByIdQuery request, Cancellati var audioDto = _mapper.Map(audio); - audioDto.Base64 = _blobService.FindFileInStorageAsBase64(audioDto.BlobName); + var audioBase64 = await _blobService.FindFileInStorageAsBase64(audioDto.BlobName); - return Result.Ok(audioDto); + if (audioBase64 is not null) + { + audioDto.Base64 = audioBase64; + return Result.Ok(audioDto); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Audio), + audioDto.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetByStreetcodeId/GetAudioByStreetcodeIdQueryHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetByStreetcodeId/GetAudioByStreetcodeIdQueryHandler.cs index 9ba66a9..5142062 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetByStreetcodeId/GetAudioByStreetcodeIdQueryHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Audio/GetByStreetcodeId/GetAudioByStreetcodeIdQueryHandler.cs @@ -43,8 +43,18 @@ public async Task> Handle(GetAudioByStreetcodeIdQuery request, var audioDto = _mapper.Map(audio); - audioDto.Base64 = _blobService.FindFileInStorageAsBase64(audioDto.BlobName); + var audioBase64 = await _blobService.FindFileInStorageAsBase64(audioDto.BlobName); + if (audioBase64 is not null) + { + audioDto.Base64 = audioBase64; + return Result.Ok(audioDto); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Audio), + audioDto.BlobName); - return Result.Ok(audioDto); + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Image/Create/CreateImageHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Image/Create/CreateImageHandler.cs index b566925..a134673 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Image/Create/CreateImageHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Image/Create/CreateImageHandler.cs @@ -31,7 +31,7 @@ public CreateImageHandler( public async Task> Handle(CreateImageCommand request, CancellationToken cancellationToken) { - string hashBlobStorageName = _blobService.SaveFileInStorage( + string hashBlobStorageName = await _blobService.SaveFileInStorage( request.Image.BaseFormat, request.Image.Title, request.Image.Extension); @@ -42,18 +42,27 @@ public async Task> Handle(CreateImageCommand request, Cancellat await _repositoryWrapper.ImageRepository.CreateAsync(image); var resultIsSuccess = await _repositoryWrapper.SaveChangesAsync() > 0; + if (!resultIsSuccess) + { + var errorMsg = Messages.Error_FailedToCreateEntity.Format(nameof(DAL.Entities.Media.Images.Image)); + _logger.LogError(request, errorMsg); + return Result.Fail(new Error(errorMsg)); + } var createdImage = _mapper.Map(image); - createdImage.Base64 = _blobService.FindFileInStorageAsBase64(createdImage.BlobName); - - if (resultIsSuccess) + var imageBase64 = await _blobService.FindFileInStorageAsBase64(createdImage.BlobName); + if (imageBase64 is not null) { + createdImage.Base64 = imageBase64; return Result.Ok(createdImage); } - var errorMsg = Messages.Error_FailedToCreateEntity.Format(nameof(DAL.Entities.Media.Images.Image)); - _logger.LogError(request, errorMsg); - return Result.Fail(new Error(errorMsg)); + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + createdImage.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetAll/GetAllImagesHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetAll/GetAllImagesHandler.cs index 023afff..5c6c382 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetAll/GetAllImagesHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetAll/GetAllImagesHandler.cs @@ -40,7 +40,19 @@ public async Task>> Handle(GetAllImagesQuery reques foreach (var image in imageDtos) { - image.Base64 = _blobService.FindFileInStorageAsBase64(image.BlobName); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(image.BlobName); + if (imageBase64 is not null) + { + image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(imageDtos); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetBaseImage/GetBaseImageHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetBaseImage/GetBaseImageHandler.cs index 9dbf5f7..fd4a05b 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetBaseImage/GetBaseImageHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetBaseImage/GetBaseImageHandler.cs @@ -27,7 +27,18 @@ public async Task> Handle(GetBaseImageQuery request, Cancel if (image is not null) { - return _blobStorage.FindFileInStorageAsMemoryStream(image.BlobName); + var imageMemoryStream = await _blobStorage.FindFileInStorageAsMemoryStream(image.BlobName); + if (imageMemoryStream is not null) + { + return Result.Ok(imageMemoryStream); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } var errorMsg = Messages.Error_EntityWithIdNotFound.Format(nameof(DAL.Entities.Media.Images.Image), request.Id); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetById/GetImageByIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetById/GetImageByIdHandler.cs index 6b3092b..615933c 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetById/GetImageByIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetById/GetImageByIdHandler.cs @@ -40,11 +40,23 @@ public async Task> Handle(GetImageByIdQuery request, Cancellati } var imageDto = _mapper.Map(image); - if (imageDto.BlobName != null) + if (imageDto.BlobName == null) { - imageDto.Base64 = _blobService.FindFileInStorageAsBase64(image.BlobName); + return Result.Ok(imageDto); } - return Result.Ok(imageDto); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(imageDto.BlobName); + if (imageBase64 is not null) + { + imageDto.Base64 = imageBase64; + return Result.Ok(imageDto); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + imageDto.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetByStreetcodeId/GetImageByStreetcodeIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetByStreetcodeId/GetImageByStreetcodeIdHandler.cs index 088e121..effce5f 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetByStreetcodeId/GetImageByStreetcodeIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/Image/GetByStreetcodeId/GetImageByStreetcodeIdHandler.cs @@ -52,7 +52,19 @@ public async Task>> Handle(GetImageByStreetcodeIdQu foreach (var image in imageDtos) { - image.Base64 = _blobService.FindFileInStorageAsBase64(image.BlobName); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(image.BlobName); + if (imageBase64 is not null) + { + image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(imageDtos); diff --git a/Streetcode/Streetcode.BLL/MediatR/Media/StreetcodeArt/GetByStreetcodeId/GetStreetcodeArtByStreetcodeIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Media/StreetcodeArt/GetByStreetcodeId/GetStreetcodeArtByStreetcodeIdHandler.cs index a400901..d34f6f4 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Media/StreetcodeArt/GetByStreetcodeId/GetStreetcodeArtByStreetcodeIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Media/StreetcodeArt/GetByStreetcodeId/GetStreetcodeArtByStreetcodeIdHandler.cs @@ -53,7 +53,19 @@ public async Task>> Handle(GetStreetcodeArt foreach (var artDto in artsDto) { - artDto.Art.Image.Base64 = _blobService.FindFileInStorageAsBase64(artDto.Art.Image.BlobName); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(artDto.Art.Image.BlobName); + if (imageBase64 is not null) + { + artDto.Art.Image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + artDto.Art.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(artsDto); diff --git a/Streetcode/Streetcode.BLL/MediatR/News/GetAll/GetAllNewsHandler.cs b/Streetcode/Streetcode.BLL/MediatR/News/GetAll/GetAllNewsHandler.cs index 42147cc..b8376a2 100644 --- a/Streetcode/Streetcode.BLL/MediatR/News/GetAll/GetAllNewsHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/News/GetAll/GetAllNewsHandler.cs @@ -42,10 +42,24 @@ public async Task>> Handle(GetAllNewsQuery request, foreach (var dto in newsDTOs) { - if (dto.Image is not null) + if (dto.Image is null) { - dto.Image.Base64 = _blobService.FindFileInStorageAsBase64(dto.Image.BlobName); + continue; } + + var imageBase64 = await _blobService.FindFileInStorageAsBase64(dto.Image.BlobName); + if (imageBase64 is not null) + { + dto.Image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + dto.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(newsDTOs); diff --git a/Streetcode/Streetcode.BLL/MediatR/News/GetById/GetNewsByIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/News/GetById/GetNewsByIdHandler.cs index 8542bd5..c3f2b38 100644 --- a/Streetcode/Streetcode.BLL/MediatR/News/GetById/GetNewsByIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/News/GetById/GetNewsByIdHandler.cs @@ -39,12 +39,24 @@ await _repositoryWrapper.NewsRepository.GetFirstOrDefaultAsync( return Result.Fail(errorMsg); } - if (newsDTO.Image is not null) + if (newsDTO.Image is null) { - newsDTO.Image.Base64 = _blobService.FindFileInStorageAsBase64(newsDTO.Image.BlobName); + return Result.Ok(newsDTO); } - return Result.Ok(newsDTO); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(newsDTO.Image.BlobName); + if (imageBase64 is not null) + { + newsDTO.Image.Base64 = imageBase64; + return Result.Ok(newsDTO); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + newsDTO.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/News/GetByUrl/GetNewsByUrlHandler.cs b/Streetcode/Streetcode.BLL/MediatR/News/GetByUrl/GetNewsByUrlHandler.cs index 40630ff..28905da 100644 --- a/Streetcode/Streetcode.BLL/MediatR/News/GetByUrl/GetNewsByUrlHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/News/GetByUrl/GetNewsByUrlHandler.cs @@ -39,12 +39,24 @@ await _repositoryWrapper.NewsRepository.GetFirstOrDefaultAsync( return Result.Fail(errorMsg); } - if (newsDTO.Image is not null) + if (newsDTO.Image is null) { - newsDTO.Image.Base64 = _blobService.FindFileInStorageAsBase64(newsDTO.Image.BlobName); + return Result.Ok(newsDTO); } - return Result.Ok(newsDTO); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(newsDTO.Image.BlobName); + if (imageBase64 is not null) + { + newsDTO.Image.Base64 = imageBase64; + return Result.Ok(newsDTO); + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + newsDTO.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/MediatR/News/GetNewsAndLinksByUrl/GetNewsAndLinksByUrlHandler.cs b/Streetcode/Streetcode.BLL/MediatR/News/GetNewsAndLinksByUrl/GetNewsAndLinksByUrlHandler.cs index c66430d..10a732b 100644 --- a/Streetcode/Streetcode.BLL/MediatR/News/GetNewsAndLinksByUrl/GetNewsAndLinksByUrlHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/News/GetNewsAndLinksByUrl/GetNewsAndLinksByUrlHandler.cs @@ -41,7 +41,18 @@ await _repositoryWrapper.NewsRepository.GetFirstOrDefaultAsync( if (newsDTO.Image is not null) { - newsDTO.Image.Base64 = _blobService.FindFileInStorageAsBase64(newsDTO.Image.BlobName); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(newsDTO.Image.BlobName); + if (imageBase64 is null) + { + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + newsDTO.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); + } + + newsDTO.Image.Base64 = imageBase64; } var news = (await _repositoryWrapper.NewsRepository.GetAllAsync()).ToList(); diff --git a/Streetcode/Streetcode.BLL/MediatR/News/SortedByDateTime/SortedByDateTimeHandler.cs b/Streetcode/Streetcode.BLL/MediatR/News/SortedByDateTime/SortedByDateTimeHandler.cs index 44b0b15..c452efb 100644 --- a/Streetcode/Streetcode.BLL/MediatR/News/SortedByDateTime/SortedByDateTimeHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/News/SortedByDateTime/SortedByDateTimeHandler.cs @@ -41,10 +41,24 @@ public async Task>> Handle(SortedByDateTimeQuery request, C foreach (var dto in newsDTOs) { - if (dto.Image is not null) + if (dto.Image is null) { - dto.Image.Base64 = _blobService.FindFileInStorageAsBase64(dto.Image.BlobName); + continue; } + + var imageBase64 = await _blobService.FindFileInStorageAsBase64(dto.Image.BlobName); + if (imageBase64 is not null) + { + dto.Image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + dto.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(newsDTOs); diff --git a/Streetcode/Streetcode.BLL/MediatR/News/Update/UpdateNewsHandler.cs b/Streetcode/Streetcode.BLL/MediatR/News/Update/UpdateNewsHandler.cs index 2dde74b..5d739f0 100644 --- a/Streetcode/Streetcode.BLL/MediatR/News/Update/UpdateNewsHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/News/Update/UpdateNewsHandler.cs @@ -1,9 +1,11 @@ using AutoMapper; using FluentResults; using MediatR; +using Microsoft.EntityFrameworkCore; using Streetcode.BLL.DTO.News; using Streetcode.BLL.Interfaces.BlobStorage; using Streetcode.BLL.Interfaces.Logging; +using Streetcode.DAL.Entities.Media.Images; using Streetcode.DAL.Repositories.Interfaces.Base; using Streetcode.Resources; using Streetcode.Shared.Extensions; @@ -14,53 +16,114 @@ public class UpdateNewsHandler : IRequestHandler> Handle(UpdateNewsCommand request, CancellationToken cancellationToken) { - var news = _mapper.Map(request.News); - if (news is null) + if (request.News is null) { var errorConvertMsg = Messages.Error_ConvertNullToEntity.Format(nameof(DAL.Entities.News.News)); _logger.LogError(request, errorConvertMsg); return Result.Fail(new Error(errorConvertMsg)); } - var response = _mapper.Map(news); + var newsEntity = await _repositoryWrapper.NewsRepository.GetFirstOrDefaultAsync( + n => n.Id == request.News.Id, + x => x.Include(n => n.Image), + true); - if (news.Image is not null) + if (newsEntity is null) { - response.Image.Base64 = _blobSevice.FindFileInStorageAsBase64(response.Image.BlobName); + var errorNotFoundMsg = Messages.Error_EntityWithIdNotFound.Format( + nameof(DAL.Entities.News.News), + request.News.Id); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } - else + + var isImageDeleted = false; + if (request.News.Image is not null && newsEntity.ImageId != request.News.ImageId) { var img = await _repositoryWrapper.ImageRepository - .GetFirstOrDefaultAsync(x => x.Id == response.ImageId); - if (img != null) + .GetFirstOrDefaultAsync( + x => x.Id == request.News.Image.Id, + trackEntities: true); + + if (img is null) { - _repositoryWrapper.ImageRepository.Delete(img); + var errorNotFoundMsg = Messages.Error_EntityWithIdNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + request.News.Image.Id); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(errorNotFoundMsg); } + + var imageBase64 = await _blobService.FindFileInStorageAsBase64(request.News.Image.BlobName); + if (imageBase64 is null) + { + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + request.News.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); + } + + request.News.Image.Base64 = imageBase64; + isImageDeleted = await DeleteImageAsync(newsEntity.Image); + } + else if (request.News.Image is null) + { + isImageDeleted = await DeleteImageAsync(newsEntity.Image); } - _repositoryWrapper.NewsRepository.Update(news); + var oldBlobName = newsEntity.Image?.BlobName; + _mapper.Map(request.News, newsEntity); + + _repositoryWrapper.NewsRepository.Update(newsEntity); var resultIsSuccess = await _repositoryWrapper.SaveChangesAsync() > 0; if (resultIsSuccess) { - return Result.Ok(response); + if (isImageDeleted) + { + await _blobService.DeleteFileInStorage(oldBlobName); + } + + return Result.Ok(_mapper.Map(newsEntity)); } var errorMsg = Messages.Error_FailedToUpdateEntity.Format(nameof(DAL.Entities.News.News)); _logger.LogError(request, errorMsg); return Result.Fail(new Error(errorMsg)); } + + private async Task DeleteImageAsync(Image? image) + { + if (image is null) + { + return false; + } + + var fact = await _repositoryWrapper.FactRepository.GetFirstOrDefaultAsync(f => f.ImageId == image.Id); + + if (fact is not null) + { + return false; + } + + _repositoryWrapper.ImageRepository.Delete(image); + return true; + } } } diff --git a/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetAll/GetAllCategoriesHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetAll/GetAllCategoriesHandler.cs index 6a0bae0..5d45a6b 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetAll/GetAllCategoriesHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetAll/GetAllCategoriesHandler.cs @@ -44,7 +44,19 @@ public async Task>> Handle(GetAllCateg foreach (var dto in dtos) { - dto.Image.Base64 = _blobService.FindFileInStorageAsBase64(dto.Image.BlobName); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(dto.Image.BlobName); + if (imageBase64 is not null) + { + dto.Image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + dto.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(dtos); diff --git a/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoriesByStreetcodeId/GetCategoriesByStreetcodeIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoriesByStreetcodeId/GetCategoriesByStreetcodeIdHandler.cs index 15d4dcc..756e5c5 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoriesByStreetcodeId/GetCategoriesByStreetcodeIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoriesByStreetcodeId/GetCategoriesByStreetcodeIdHandler.cs @@ -54,7 +54,19 @@ public async Task>> Handle( foreach (var srcCategory in mappedSrcCategories) { - srcCategory.Image.Base64 = _blobService.FindFileInStorageAsBase64(srcCategory.Image.BlobName); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(srcCategory.Image.BlobName); + if (imageBase64 is not null) + { + srcCategory.Image.Base64 = imageBase64; + continue; + } + + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + srcCategory.Image.BlobName); + + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } return Result.Ok(mappedSrcCategories); diff --git a/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoryById/GetCategoryByIdHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoryById/GetCategoryByIdHandler.cs index 2e6a139..84b2f12 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoryById/GetCategoryByIdHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Sources/SourceLinkCategory/GetCategoryById/GetCategoryByIdHandler.cs @@ -32,14 +32,14 @@ public GetCategoryByIdHandler( public async Task> Handle(GetCategoryByIdQuery request, CancellationToken cancellationToken) { - var srcCategories = await _repositoryWrapper + var srcCategory = await _repositoryWrapper .SourceCategoryRepository.GetFirstOrDefaultAsync( predicate: sc => sc.Id == request.Id, include: scl => scl .Include(sc => sc.StreetcodeCategoryContents) .Include(sc => sc.Image) !); - if (srcCategories is null) + if (srcCategory is null) { var errorMsg = Messages.Error_EntityWithIdNotFound.Format( nameof(DAL.Entities.Sources.SourceLinkCategory), @@ -49,10 +49,19 @@ public async Task> Handle(GetCategoryByIdQuery req return Result.Fail(new Error(errorMsg)); } - var mappedSrcCategories = _mapper.Map(srcCategories); + var mappedSrcCategory = _mapper.Map(srcCategory); + var imageBase64 = await _blobService.FindFileInStorageAsBase64(mappedSrcCategory.Image.BlobName); + if (imageBase64 is not null) + { + mappedSrcCategory.Image.Base64 = imageBase64; + return Result.Ok(mappedSrcCategory); + } - mappedSrcCategories.Image.Base64 = _blobService.FindFileInStorageAsBase64(mappedSrcCategories.Image.BlobName); + var errorNotFoundMsg = Messages.Error_MediaBlobNotFound.Format( + nameof(DAL.Entities.Media.Images.Image), + mappedSrcCategory.Image.BlobName); - return Result.Ok(mappedSrcCategories); + _logger.LogError(request, errorNotFoundMsg); + return Result.Fail(new Error(errorNotFoundMsg)); } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Services/BlobStorageService/AzureBlobEnvironmentVariables.cs b/Streetcode/Streetcode.BLL/Services/BlobStorageService/AzureBlobEnvironmentVariables.cs new file mode 100644 index 0000000..13dead3 --- /dev/null +++ b/Streetcode/Streetcode.BLL/Services/BlobStorageService/AzureBlobEnvironmentVariables.cs @@ -0,0 +1,7 @@ +namespace Streetcode.BLL.Services.BlobStorageService; + +public class AzureBlobEnvironmentVariables +{ + public string ContainerName { get; set; } + public string EncryptionKey { get; set; } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Services/BlobStorageService/AzureBlobService.cs b/Streetcode/Streetcode.BLL/Services/BlobStorageService/AzureBlobService.cs new file mode 100644 index 0000000..2c1b904 --- /dev/null +++ b/Streetcode/Streetcode.BLL/Services/BlobStorageService/AzureBlobService.cs @@ -0,0 +1,177 @@ +using System.Diagnostics.CodeAnalysis; +using Azure; +using Azure.Storage.Blobs; +using Microsoft.Extensions.Options; +using Streetcode.BLL.Interfaces.BlobStorage; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.DAL.Repositories.Interfaces.Base; +using Streetcode.Shared.Services; + +namespace Streetcode.BLL.Services.BlobStorageService; + +public class AzureBlobService : IBlobService +{ + private readonly AzureBlobEnvironmentVariables _options; + private readonly IRepositoryWrapper _repositoryWrapper; + private readonly BlobServiceClient _blobServiceClient; + private readonly ILoggerService _logger; + + public AzureBlobService( + IOptions options, + IRepositoryWrapper repositoryWrapper, + BlobServiceClient blobServiceClient, + ILoggerService logger) + { + _options = options.Value; + _repositoryWrapper = repositoryWrapper; + _blobServiceClient = blobServiceClient; + _logger = logger; + } + + public async Task SaveFileInStorage(string base64, string name, string extension) + { + byte[] imageBytes = Convert.FromBase64String(base64); + string createdFileName = FileService.PrepareFileStorageName(name); + + string hashBlobName = FileService.HashFunction(createdFileName); + string fullBlobName = $"{hashBlobName}.{extension}"; + + byte[] encryptedData = FileService.EncryptBytes(imageBytes, _options.EncryptionKey); + + var containerClient = _blobServiceClient.GetBlobContainerClient(_options.ContainerName); + await containerClient.CreateIfNotExistsAsync(); + + var blobClient = containerClient.GetBlobClient(fullBlobName); + + using (var ms = new MemoryStream(encryptedData)) + { + await blobClient.UploadAsync(ms, overwrite: true); + } + + return hashBlobName; + } + + public async Task FindFileInStorageAsMemoryStream(string name) + { + var bytes = await FindFileInStorageAsBytes(name); + return bytes is null ? null : new MemoryStream(bytes); + } + + public async Task FindFileInStorageAsBase64(string name) + { + var bytes = await FindFileInStorageAsBytes(name); + return bytes is null ? null : Convert.ToBase64String(bytes); + } + + public async Task UpdateFileInStorage( + string previousBlobName, + string base64Format, + string newBlobName, + string extension) + { + await DeleteFileInStorage(previousBlobName); + + string hashBlobStorageName = await SaveFileInStorage( + base64Format, + newBlobName, + extension); + + return hashBlobStorageName; + } + + public async Task DeleteFileInStorage(string name) + { + BlobContainerClient containerClient = _blobServiceClient.GetBlobContainerClient(_options.ContainerName); + BlobClient blobClient = containerClient.GetBlobClient(name); + _logger.LogInformation($"Deleting {name} from Azure..."); + await blobClient.DeleteIfExistsAsync(); + } + + public async Task CleanBlobStorage() + { + const int BatchSize = 250; + var containerClient = _blobServiceClient.GetBlobContainerClient(_options.ContainerName); + + List currentBatch = new (); + + await foreach (var blobItem in containerClient.GetBlobsAsync()) + { + currentBatch.Add(blobItem.Name); + + // When we hit our limit, process the batch + if (currentBatch.Count >= BatchSize) + { + await ProcessBatch(currentBatch, containerClient); + currentBatch.Clear(); + } + } + + if (currentBatch.Any()) + { + await ProcessBatch(currentBatch, containerClient); + } + } + + private async Task ProcessBatch(List blobNames, BlobContainerClient container) + { + try + { + var foundInImages = (await _repositoryWrapper.ImageRepository + .GetAllAsync(img => blobNames.Contains(img.BlobName))) + .Select(img => img.BlobName) + .ToList(); + + var foundInAudios = (await _repositoryWrapper.AudioRepository + .GetAllAsync(aud => blobNames.Contains(aud.BlobName))) + .Select(aud => aud.BlobName) + .ToList(); + + var existingInDb = foundInImages.Concat(foundInAudios).ToHashSet(); + + var orphans = blobNames.Except(existingInDb); + + foreach (var orphan in orphans) + { + try + { + _logger.LogInformation($"Deleting orphaned blob: {orphan}"); + await container.GetBlobClient(orphan).DeleteIfExistsAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to delete blob {orphan}. Skipping..."); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error during blob cleanup: {ex.Message}"); + } + } + + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:ClosingSquareBracketsMustBeSpacedCorrectly", Justification = "Reviewed.")] + private async Task FindFileInStorageAsBytes(string name) + { + var containerClient = _blobServiceClient.GetBlobContainerClient(_options.ContainerName); + + var blobClient = containerClient.GetBlobClient(name); + + try + { + using var ms = new MemoryStream(); + await blobClient.DownloadToAsync(ms); + byte[] encryptedBytes = ms.ToArray(); + + return FileService.DecryptBytes(encryptedBytes, _options.EncryptionKey); + } + catch (RequestFailedException ex) + { + if (ex.Status == 404) + { + return null; + } + + throw; + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Services/BlobStorageService/BlobService.cs b/Streetcode/Streetcode.BLL/Services/BlobStorageService/BlobService.cs index 20cb2a9..6d77797 100644 --- a/Streetcode/Streetcode.BLL/Services/BlobStorageService/BlobService.cs +++ b/Streetcode/Streetcode.BLL/Services/BlobStorageService/BlobService.cs @@ -1,88 +1,93 @@ -using System.Security.Cryptography; -using System.Text; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Options; using Streetcode.BLL.Interfaces.BlobStorage; using Streetcode.DAL.Repositories.Interfaces.Base; +using Streetcode.Shared.Services; namespace Streetcode.BLL.Services.BlobStorageService; public class BlobService : IBlobService { - private readonly BlobEnvironmentVariables _envirovment; private readonly string _keyCrypt; private readonly string _blobPath; private readonly IRepositoryWrapper _repositoryWrapper; public BlobService(IOptions environment, IRepositoryWrapper? repositoryWrapper = null) { - _envirovment = environment.Value; - _keyCrypt = _envirovment.BlobStoreKey; - _blobPath = _envirovment.BlobStorePath; + _keyCrypt = environment.Value.BlobStoreKey; + _blobPath = environment.Value.BlobStorePath; _repositoryWrapper = repositoryWrapper; } - public MemoryStream FindFileInStorageAsMemoryStream(string name) + public Task FindFileInStorageAsMemoryStream(string name) { - string[] splitedName = name.Split('.'); + var decodedBytes = GetDecryptedFile(name); - byte[] decodedBytes = DecryptFile(splitedName[0], splitedName[1]); + if (decodedBytes == null) + { + return Task.FromResult(null!); + } var image = new MemoryStream(decodedBytes); - return image; + return Task.FromResult(image); } - public string FindFileInStorageAsBase64(string name) + public Task FindFileInStorageAsBase64(string name) { - string[] splitedName = name.Split('.'); + var decodedBytes = GetDecryptedFile(name); - byte[] decodedBytes = DecryptFile(splitedName[0], splitedName[1]); + if (decodedBytes == null) + { + return Task.FromResult(null!); + } string base64 = Convert.ToBase64String(decodedBytes); - return base64; + return Task.FromResult(base64); } - public string SaveFileInStorage(string base64, string name, string extension) + public Task SaveFileInStorage(string base64, string name, string extension) { byte[] imageBytes = Convert.FromBase64String(base64); - string createdFileName = $"{DateTime.Now}{name}" - .Replace(" ", "_") - .Replace(".", "_") - .Replace(":", "_"); + string createdFileName = FileService.PrepareFileStorageName(name); - string hashBlobStorageName = HashFunction(createdFileName); + string hashBlobStorageName = FileService.HashFunction(createdFileName); Directory.CreateDirectory(_blobPath); - EncryptFile(imageBytes, extension, hashBlobStorageName); + byte[] encryptedData = FileService.EncryptBytes(imageBytes, _keyCrypt); + File.WriteAllBytes($"{_blobPath}{hashBlobStorageName}.{extension}", encryptedData); - return hashBlobStorageName; + return Task.FromResult(hashBlobStorageName); } - public void SaveFileInStorageBase64(string base64, string name, string extension) + public Task SaveFileInStorageBase64(string base64, string name, string extension) { byte[] imageBytes = Convert.FromBase64String(base64); Directory.CreateDirectory(_blobPath); - EncryptFile(imageBytes, extension, name); + var encryptedBytes = FileService.EncryptBytes(imageBytes, _keyCrypt); + File.WriteAllBytes($"{_blobPath}{name}.{extension}", encryptedBytes); + return Task.CompletedTask; } - public void DeleteFileInStorage(string name) + public Task DeleteFileInStorage(string name) { File.Delete($"{_blobPath}{name}"); + return Task.CompletedTask; } - public string UpdateFileInStorage( + public async Task UpdateFileInStorage( string previousBlobName, string base64Format, string newBlobName, string extension) { - DeleteFileInStorage(previousBlobName); + await DeleteFileInStorage(previousBlobName); - string hashBlobStorageName = SaveFileInStorage( - base64Format, - newBlobName, - extension); + string hashBlobStorageName = await SaveFileInStorage( + base64Format, + newBlobName, + extension); return hashBlobStorageName; } @@ -103,7 +108,7 @@ public async Task CleanBlobStorage() foreach (var file in filesToRemove) { Console.WriteLine($"Deleting {file}..."); - DeleteFileInStorage(file); + await DeleteFileInStorage(file); } } @@ -114,60 +119,15 @@ private IEnumerable GetAllBlobNames() return paths.Select(p => Path.GetFileName(p)); } - private string HashFunction(string createdFileName) - { - using (var hash = SHA256.Create()) - { - Encoding enc = Encoding.UTF8; - byte[] result = hash.ComputeHash(enc.GetBytes(createdFileName)); - return Convert.ToBase64String(result).Replace('/', '_'); - } - } - - private void EncryptFile(byte[] imageBytes, string type, string name) - { - byte[] keyBytes = Encoding.UTF8.GetBytes(_keyCrypt); - - byte[] iv = new byte[16]; - using (var rng = new RNGCryptoServiceProvider()) - { - rng.GetBytes(iv); - } - - byte[] encryptedBytes; - using (Aes aes = Aes.Create()) - { - aes.KeySize = 256; - aes.Key = keyBytes; - aes.IV = iv; - ICryptoTransform encryptor = aes.CreateEncryptor(); - encryptedBytes = encryptor.TransformFinalBlock(imageBytes, 0, imageBytes.Length); - } - - byte[] encryptedData = new byte[encryptedBytes.Length + iv.Length]; - Buffer.BlockCopy(iv, 0, encryptedData, 0, iv.Length); - Buffer.BlockCopy(encryptedBytes, 0, encryptedData, iv.Length, encryptedBytes.Length); - File.WriteAllBytes($"{_blobPath}{name}.{type}", encryptedData); - } - - private byte[] DecryptFile(string fileName, string type) + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:ClosingSquareBracketsMustBeSpacedCorrectly", Justification = "Reviewed.")] + private byte[]? GetDecryptedFile(string name) { - byte[] encryptedData = File.ReadAllBytes($"{_blobPath}{fileName}.{type}"); - byte[] keyBytes = Encoding.UTF8.GetBytes(_keyCrypt); - - byte[] iv = new byte[16]; - Buffer.BlockCopy(encryptedData, 0, iv, 0, iv.Length); - - byte[] decryptedBytes; - using (Aes aes = Aes.Create()) + if (!File.Exists($"{_blobPath}{name}")) { - aes.KeySize = 256; - aes.Key = keyBytes; - aes.IV = iv; - ICryptoTransform decryptor = aes.CreateDecryptor(); - decryptedBytes = decryptor.TransformFinalBlock(encryptedData, iv.Length, encryptedData.Length - iv.Length); + return null; } - return decryptedBytes; + byte[] encryptedData = File.ReadAllBytes($"{_blobPath}{name}"); + return FileService.DecryptBytes(encryptedData, _keyCrypt); } } \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Streetcode.BLL.csproj b/Streetcode/Streetcode.BLL/Streetcode.BLL.csproj index 483e945..1054ee7 100644 --- a/Streetcode/Streetcode.BLL/Streetcode.BLL.csproj +++ b/Streetcode/Streetcode.BLL/Streetcode.BLL.csproj @@ -19,6 +19,7 @@ + diff --git a/Streetcode/Streetcode.Resources/Messages.Designer.cs b/Streetcode/Streetcode.Resources/Messages.Designer.cs index 79f6ce4..44ab788 100644 --- a/Streetcode/Streetcode.Resources/Messages.Designer.cs +++ b/Streetcode/Streetcode.Resources/Messages.Designer.cs @@ -222,6 +222,15 @@ public static string Error_MissingUserId { } } + /// + /// Looks up a localized string similar to {0} blob with BlobName: "{1}" not found. + /// + public static string Error_MediaBlobNotFound { + get { + return ResourceManager.GetString("Error_MediaBlobNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to News with Url: {0} not found. /// diff --git a/Streetcode/Streetcode.Resources/Messages.resx b/Streetcode/Streetcode.Resources/Messages.resx index be6cc40..a27d419 100644 --- a/Streetcode/Streetcode.Resources/Messages.resx +++ b/Streetcode/Streetcode.Resources/Messages.resx @@ -216,4 +216,7 @@ No RelatedTerm found by TermId: {0} + + {0} blob with BlobName: "{1}" not found + \ No newline at end of file diff --git a/Streetcode/Streetcode.Shared/Extensions/AsyncEnumerableExtensions.cs b/Streetcode/Streetcode.Shared/Extensions/AsyncEnumerableExtensions.cs new file mode 100644 index 0000000..8da6938 --- /dev/null +++ b/Streetcode/Streetcode.Shared/Extensions/AsyncEnumerableExtensions.cs @@ -0,0 +1,14 @@ +namespace Streetcode.Shared.Extensions; + +public static class AsyncEnumerableExtensions +{ + public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable enumeration) + { + foreach (var item in enumeration) + { + yield return item; + } + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Shared/Services/FileService.cs b/Streetcode/Streetcode.Shared/Services/FileService.cs new file mode 100644 index 0000000..5e856a8 --- /dev/null +++ b/Streetcode/Streetcode.Shared/Services/FileService.cs @@ -0,0 +1,74 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Streetcode.Shared.Services; + +public static class FileService +{ + public static string HashFunction(string createdFileName) + { + using (var hash = SHA256.Create()) + { + Encoding enc = Encoding.UTF8; + byte[] result = hash.ComputeHash(enc.GetBytes(createdFileName)); + return Convert.ToBase64String(result).Replace('/', '_'); + } + } + + public static byte[] EncryptBytes(byte[] plainBytes, string keyCrypt) + { + byte[] keyBytes = Encoding.UTF8.GetBytes(keyCrypt); + byte[] iv = new byte[16]; + + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(iv); + } + + using (Aes aes = Aes.Create()) + { + aes.KeySize = 256; + aes.Key = keyBytes; + aes.IV = iv; + + using (ICryptoTransform encryptor = aes.CreateEncryptor()) + { + byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); + + byte[] encryptedData = new byte[iv.Length + encryptedBytes.Length]; + Buffer.BlockCopy(iv, 0, encryptedData, 0, iv.Length); + Buffer.BlockCopy(encryptedBytes, 0, encryptedData, iv.Length, encryptedBytes.Length); + + return encryptedData; + } + } + } + + public static byte[] DecryptBytes(byte[] encryptedData, string keyCrypt) + { + byte[] keyBytes = Encoding.UTF8.GetBytes(keyCrypt); + + byte[] iv = new byte[16]; + Buffer.BlockCopy(encryptedData, 0, iv, 0, iv.Length); + + byte[] decryptedBytes; + using (Aes aes = Aes.Create()) + { + aes.KeySize = 256; + aes.Key = keyBytes; + aes.IV = iv; + ICryptoTransform decryptor = aes.CreateDecryptor(); + decryptedBytes = decryptor.TransformFinalBlock(encryptedData, iv.Length, encryptedData.Length - iv.Length); + } + + return decryptedBytes; + } + + public static string PrepareFileStorageName(string name) + { + return $"{DateTime.Now}{name}" + .Replace(" ", "_") + .Replace(".", "_") + .Replace(":", "_"); + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.WebApi/Extensions/ConfigureHostBuilderExtensions.cs b/Streetcode/Streetcode.WebApi/Extensions/ConfigureHostBuilderExtensions.cs index 0624b45..cc50004 100644 --- a/Streetcode/Streetcode.WebApi/Extensions/ConfigureHostBuilderExtensions.cs +++ b/Streetcode/Streetcode.WebApi/Extensions/ConfigureHostBuilderExtensions.cs @@ -20,6 +20,7 @@ public static void ConfigureApplication(this ConfigureHostBuilder host) public static void ConfigureBlob(this IServiceCollection services, WebApplicationBuilder builder) { services.Configure(builder.Configuration.GetSection("Blob")); + services.Configure(builder.Configuration.GetSection("AzureBlobStorage")); } public static void ConfigurePayment(this IServiceCollection services, WebApplicationBuilder builder) diff --git a/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs index d48a9ae..997d359 100644 --- a/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ using Streetcode.DAL.Persistence; using Streetcode.DAL.Repositories.Interfaces.Base; using Streetcode.DAL.Repositories.Realizations.Base; +using Microsoft.Extensions.Azure; namespace Streetcode.WebApi.Extensions; @@ -37,7 +38,10 @@ public static void AddRepositoryServices(this IServiceCollection services) services.AddScoped(); } - public static void AddCustomServices(this IServiceCollection services, IConfiguration configuration) + public static void AddCustomServices( + this IServiceCollection services, + IConfiguration configuration, + IWebHostEnvironment environment) { services.AddRepositoryServices(); services.AddFeatureManagement(); @@ -49,7 +53,20 @@ public static void AddCustomServices(this IServiceCollection services, IConfigur services.AddValidatorsFromAssemblies(currentAssemblies); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>)); - services.AddScoped(); + if (environment.IsEnvironment("Local")) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + services.AddAzureClients(clientBuilder => + { + var connectionString = configuration.GetSection("AzureBlobStorage")["ConnectionString"]; + clientBuilder.AddBlobServiceClient(connectionString); + }); + } + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Streetcode/Streetcode.WebApi/Program.cs b/Streetcode/Streetcode.WebApi/Program.cs index 324e9dd..c72ee7c 100644 --- a/Streetcode/Streetcode.WebApi/Program.cs +++ b/Streetcode/Streetcode.WebApi/Program.cs @@ -1,4 +1,5 @@ using Hangfire; +using Streetcode.BLL.Interfaces.BlobStorage; using Streetcode.BLL.Services.BlobStorageService; using Streetcode.WebApi.Extensions; using Streetcode.WebApi.Middleware; @@ -11,7 +12,7 @@ builder.Services.AddJwtAuthentication(builder.Configuration); builder.Services.AddSwaggerServices(); builder.Services.AddRedisCacheServices(builder.Configuration); -builder.Services.AddCustomServices(builder.Configuration); +builder.Services.AddCustomServices(builder.Configuration, builder.Environment); builder.Services.ConfigureBlob(builder); builder.Services.ConfigurePayment(builder); builder.Services.ConfigureInstagram(builder); @@ -47,9 +48,13 @@ BackgroundJob.Schedule( wp => wp.ParseZipFileFromWebAsync(bypassSslValidation), TimeSpan.FromMinutes(1)); RecurringJob.AddOrUpdate( - wp => wp.ParseZipFileFromWebAsync(bypassSslValidation), Cron.Monthly); - RecurringJob.AddOrUpdate( - b => b.CleanBlobStorage(), Cron.Monthly); + "parse-zip-file-job", + wp => wp.ParseZipFileFromWebAsync(bypassSslValidation), + Cron.Monthly); + RecurringJob.AddOrUpdate( + "clean-blobs-job", + b => b.CleanBlobStorage(), + Cron.Monthly); } app.MapControllers(); diff --git a/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj b/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj index 9b5d161..d2d9b83 100644 --- a/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj +++ b/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj @@ -30,6 +30,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Streetcode/Streetcode.WebApi/appsettings.json b/Streetcode/Streetcode.WebApi/appsettings.json index 8c3822d..a28dc5d 100644 --- a/Streetcode/Streetcode.WebApi/appsettings.json +++ b/Streetcode/Streetcode.WebApi/appsettings.json @@ -41,6 +41,11 @@ "BlobStoreKey": "SlavaKasterovSuperGoodInshalaKey", "BlobStorePath": "../../BlobStorageFolder/" }, + "AzureBlobStorage": { + "ConnectionString": "azureblobconnectionstring", + "ContainerName": "default", + "EncryptionKey": "super__encryptionkey_32byteslong" + }, "Payment": { "Token": "BombasticTokenForMonobank" }, diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Art/GetArtsByStreetcodeIdHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Art/GetArtsByStreetcodeIdHandlerTests.cs index 3c97b3e..f28aa7f 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Art/GetArtsByStreetcodeIdHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Art/GetArtsByStreetcodeIdHandlerTests.cs @@ -143,6 +143,27 @@ public async Task Handle_ReturnsFail_WhenArtsAreEmpty(int streetcodeId) result.Errors[0].Message); } + [Fact] + public async Task Handle_ReturnsFail_WhenBlobNotExists() + { + // Arrange + var streetcodeId = 1; + List arts = GetArtsList(); + + this.SetupArts(arts); + this.SetupBlobService(null); + + // Act + var result = await this.handler + .Handle(new GetArtsByStreetcodeIdQuery(streetcodeId), CancellationToken.None); + + // Assert + Assert.True(result.IsFailed); + Assert.Equal( + Messages.Error_MediaBlobNotFound.Format(nameof(Image), arts[0].Image!.BlobName!), + result.Errors[0].Message); + } + private void SetupArts(List? arts) { var artRepository = new Mock(); @@ -158,8 +179,9 @@ private void SetupArts(List? arts) private void SetupBlobService(string base64String) { - this.blobServiceMock.Setup(b => b.FindFileInStorageAsBase64(It.IsAny())) - .Returns(base64String); + this.blobServiceMock + .Setup(b => b.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync(base64String); } private static List GetArtsList() diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Create/CreateAudioHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Create/CreateAudioHandlerTests.cs index f119093..06c8c36 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Create/CreateAudioHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Create/CreateAudioHandlerTests.cs @@ -56,7 +56,7 @@ public async Task Handle_ValidData_ReturnsOkResult() this.mockBlobService .Setup(b => b.SaveFileInStorage(createDto.BaseFormat, createDto.Title, createDto.Extension)) - .Returns(hashName); + .ReturnsAsync(hashName); this.mockRepositoryWrapper .Setup(r => r.AudioRepository.CreateAsync(It.IsAny())) @@ -103,7 +103,7 @@ public async Task Handle_DatabaseSaveFails_ReturnsFailResult() this.mockBlobService .Setup(b => b.SaveFileInStorage(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns("some-hash"); + .ReturnsAsync("some-hash"); this.mockRepositoryWrapper .Setup(r => r.AudioRepository.CreateAsync(It.IsAny())) @@ -121,7 +121,6 @@ public async Task Handle_DatabaseSaveFails_ReturnsFailResult() // Assert result.IsFailed.Should().BeTrue(); result.Errors.First().Message.Should().Be(expectedErrorMsg); - } } } \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAllAudiosHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAllAudiosHandlerTests.cs index 8761bfd..e9f36e2 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAllAudiosHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAllAudiosHandlerTests.cs @@ -1,4 +1,8 @@ -namespace Streetcode.XUnitTest.MediatR.Media.Audio.Get { +using Streetcode.Shared.Extensions; + +namespace Streetcode.XUnitTest.MediatR.Media.Audio.Get +{ + using System.Linq.Expressions; using AutoMapper; using FluentAssertions; using Microsoft.EntityFrameworkCore.Query; @@ -10,7 +14,6 @@ using Streetcode.BLL.MediatR.Media.Audio.GetAll; using Streetcode.DAL.Repositories.Interfaces.Base; using Streetcode.Resources; - using System.Linq.Expressions; using Xunit; using AudioEntity = Streetcode.DAL.Entities.Media.Audio; @@ -71,10 +74,10 @@ public async Task Handle_AudiosExist_ReturnsOkResultWithBase64() // Arrange var query = new GetAllAudiosQuery(); var audios = new List - { - new () { Id = 1, BlobName = "audio1.mp3" }, - new () { Id = 2, BlobName = "audio2.mp3" } - }; + { + new () { Id = 1, BlobName = "audio1.mp3" }, + new () { Id = 2, BlobName = "audio2.mp3" } + }; this.mockRepositoryWrapper .Setup(r => r.AudioRepository.GetAllAsync( @@ -85,7 +88,7 @@ public async Task Handle_AudiosExist_ReturnsOkResultWithBase64() this.mockBlobService .Setup(b => b.FindFileInStorageAsBase64(It.IsAny())) - .Returns((string blobName) => $"base64-content-of-{blobName}"); + .ReturnsAsync((string blobName) => $"base64-content-of-{blobName}"); // Act var result = await this.handler.Handle(query, CancellationToken.None); @@ -107,7 +110,11 @@ public async Task Handle_ReturnsCorrectType() It.IsAny>>(), It.IsAny, IIncludableQueryable>>(), It.IsAny())) - .ReturnsAsync(new List { new() }); + .ReturnsAsync(new List { new () }); + + this.mockBlobService + .Setup(b => b.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync("base64"); // Act var result = await this.handler.Handle(new GetAllAudiosQuery(), CancellationToken.None); @@ -115,5 +122,37 @@ public async Task Handle_ReturnsCorrectType() // Assert result.Value.Should().BeAssignableTo>(); } + + [Fact] + public async Task Handle_BlobNotFound_ReturnsFailResult() + { + // Arrange + var query = new GetAllAudiosQuery(); + var audios = new List + { + new () { Id = 1, BlobName = "audio1.mp3" }, + new () { Id = 2, BlobName = "audio2.mp3" }, + }; + + this.mockRepositoryWrapper + .Setup(r => r.AudioRepository.GetAllAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(audios); + + this.mockBlobService + .Setup(b => b.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync((string blobName) => null); + + // Act + var result = await this.handler.Handle(query, CancellationToken.None); + + // Assert + result.IsFailed.Should().BeTrue(); + result.Errors.Should().ContainSingle(Messages.Error_MediaBlobNotFound.Format( + nameof(AudioEntity), + audios[0].BlobName!)); + } } } \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByIdHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByIdHandlerTests.cs index 701472f..0059f10 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByIdHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByIdHandlerTests.cs @@ -62,7 +62,7 @@ public async Task Handle_AudioExists_ReturnsOkResultWithAudioDTO() this.mockBlobService .Setup(b => b.FindFileInStorageAsBase64(audioEntity.BlobName)) - .Returns(expectedBase64); + .ReturnsAsync(expectedBase64); // Act var result = await this.handler.Handle(query, CancellationToken.None); diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByStreetcodeIdHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByStreetcodeIdHandlerTests.cs index 23f6267..6524d62 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByStreetcodeIdHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetAudioByStreetcodeIdHandlerTests.cs @@ -88,7 +88,7 @@ public async Task Handle_AudioExists_ReturnsOkResultWithBase64() this.mockBlobService .Setup(b => b.FindFileInStorageAsBase64(audioEntity.BlobName)) - .Returns(expectedBase64); + .ReturnsAsync(expectedBase64); // Act var result = await this.handler.Handle(query, CancellationToken.None); diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetBaseAudioHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetBaseAudioHandlerTests.cs index 7c453ef..3843c08 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetBaseAudioHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/Media/Audio/Get/GetBaseAudioHandlerTests.cs @@ -49,7 +49,7 @@ public async Task Handle_AudioExists_ReturnsMemoryStream() this.mockBlobService .Setup(b => b.FindFileInStorageAsMemoryStream(audioEntity.BlobName)) - .Returns(memoryStream); + .ReturnsAsync(memoryStream); // Act var result = await this.handler.Handle(query, CancellationToken.None); diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetAllNewsHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetAllNewsHandlerTests.cs index 021214a..eb9d8ef 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetAllNewsHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetAllNewsHandlerTests.cs @@ -51,11 +51,11 @@ public async Task Handle_ShouldReturnFail_WhenNoNewsInDb() { // Arrange _repositoryWrapperMock.Setup(r => r.NewsRepository.GetAllAsync( - It.IsAny>>(), - It.IsAny, IIncludableQueryable>>(), - false - )) - .ReturnsAsync([]); + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false + )) + .ReturnsAsync([]); var request = new GetAllNewsQuery(); @@ -86,11 +86,11 @@ public async Task Handle_ShouldReturnNewsDto_WhenNewsFound() }; _repositoryWrapperMock.Setup(r => r.NewsRepository.GetAllAsync( - It.IsAny>>(), - It.IsAny, IIncludableQueryable>>(), - false - )) - .ReturnsAsync(news); + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false + )) + .ReturnsAsync(news); var request = new GetAllNewsQuery(); @@ -117,19 +117,18 @@ public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsFoundWithImage() URL = "https://github.com/", CreationDate = now, Image = new Image(), - }, }; _repositoryWrapperMock.Setup(r => r.NewsRepository.GetAllAsync( - It.IsAny>>(), - It.IsAny, IIncludableQueryable>>(), - false - )) - .ReturnsAsync(news); + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false + )) + .ReturnsAsync(news); _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) - .Returns(fakeBase); + .ReturnsAsync(fakeBase); var request = new GetAllNewsQuery(); @@ -140,5 +139,44 @@ public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsFoundWithImage() result.IsSuccess.Should().BeTrue(); result.Value.FirstOrDefault().Image.Base64.Should().Be(fakeBase); } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewsFoundWithImageButBlobNotExists() + { + // Arrange + var now = DateTime.Now; + + var news = new NewsEntity + { + Title = "Test Title", + Text = "Sample text", + URL = "https://github.com/", + CreationDate = now, + Image = new Image + { + BlobName = "BlobName", + }, + }; + + _repositoryWrapperMock.Setup(r => r.NewsRepository.GetAllAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false + )) + .ReturnsAsync(new List { news }); + + _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync((string?)null); + + var request = new GetAllNewsQuery(); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + result.IsFailed.Should().BeTrue(); + result.Errors.Should().ContainSingle( + Messages.Error_MediaBlobNotFound.Format(nameof(Image), news.Image.BlobName)); + } } } diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsAndLinksByUrlHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsAndLinksByUrlHandlerTests.cs index 058e654..b604915 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsAndLinksByUrlHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsAndLinksByUrlHandlerTests.cs @@ -97,7 +97,7 @@ public async Task Handle_ShouldReturnNewsDTOWithURLsAndImage_WhenNewsExists() .ReturnsAsync(allNews); _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) - .Returns(expectedBase64); + .ReturnsAsync(expectedBase64); var request = new GetNewsAndLinksByUrlQuery(url); @@ -274,5 +274,42 @@ public async Task Handle_ShouldReturnRandomNewsMinusTwo_WhenNewsIsLastAndCountGr res.Value.RandomNews.RandomNewsUrl.Should().Be(allNews[1].URL); res.Value.RandomNews.Title.Should().Be(allNews[1].Title); } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewsFoundWithImageButBlobNotExists() + { + // Arrange + var now = DateTime.Now; + + var news = new NewsEntity + { + Title = "Test Title", + Text = "Sample text", + URL = "https://github.com/", + CreationDate = now, + Image = new Image + { + BlobName = "BlobName", + }, + }; + + var url = "https://github.com/"; + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false)) + .ReturnsAsync(news); + + _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync((string?)null); + + // Act + var res = await _handler.Handle(new GetNewsAndLinksByUrlQuery(url), CancellationToken.None); + + // Assert + res.IsFailed.Should().BeTrue(); + res.Errors.Should().ContainSingle(Messages.Error_MediaBlobNotFound.Format(nameof(Image), news.Image.BlobName)); + } } } diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByIdHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByIdHandlerTests.cs index ec1b3ef..0f0266c 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByIdHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByIdHandlerTests.cs @@ -129,7 +129,7 @@ public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsExistByIdWithImage .ReturnsAsync(news); _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) - .Returns(fakeBase); + .ReturnsAsync(fakeBase); var request = new GetNewsByIdQuery(id); @@ -140,5 +140,47 @@ public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsExistByIdWithImage result.IsSuccess.Should().BeTrue(); result.Value.Image.Base64.Should().Be(fakeBase); } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewsExistsByIdWithImageButBlobNotExists() + { + // Arrange + int id = 1; + + var now = DateTime.Now; + + var news = new NewsEntity + { + Id = id, + Title = "Test Title", + Text = "Sample text", + URL = "https://github.com/", + CreationDate = now, + Image = new Image + { + BlobName = "BlobName", + }, + }; + + _repositoryWrapperMock.Setup(r => r.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false + )) + .ReturnsAsync(news); + + _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync((string?)null); + + var request = new GetNewsByIdQuery(id); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + result.IsFailed.Should().BeTrue(); + result.Errors.Should() + .ContainSingle(Messages.Error_MediaBlobNotFound.Format(nameof(Image), news.Image.BlobName)); + } } } diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByUrlHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByUrlHandlerTests.cs index c66e59b..f71697b 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByUrlHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/News/GetNewsByUrlHandlerTests.cs @@ -54,9 +54,9 @@ public async Task Handle_ShouldReturnFail_WhenNewsNotExists() var request = new GetNewsByUrlQuery(url); _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( - It.IsAny>>(), - It.IsAny, IIncludableQueryable>>(), - false)) + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false)) .ReturnsAsync((NewsEntity)null); // Act @@ -87,13 +87,13 @@ public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsWithImageExists() var request = new GetNewsByUrlQuery(url); _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( - It.IsAny>>(), - It.IsAny, IIncludableQueryable>>(), - false)) + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false)) .ReturnsAsync(news); _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) - .Returns(expectedBase64); + .ReturnsAsync(expectedBase64); // Act var res = await _handler.Handle(request, CancellationToken.None); @@ -120,13 +120,13 @@ public async Task Handle_ShouldReturnNewsDtoWithOutImage_WhenNewsExistsButImageN CreationDate = now, }; - var url = "test"; + var url = "https://github.com/"; var request = new GetNewsByUrlQuery(url); _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( - It.IsAny>>(), - It.IsAny, IIncludableQueryable>>(), - false)) + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false)) .ReturnsAsync(news); // Act @@ -138,5 +138,41 @@ public async Task Handle_ShouldReturnNewsDtoWithOutImage_WhenNewsExistsButImageN bs => bs.FindFileInStorageAsBase64(It.IsAny()), Times.Never); } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewsWithImageExistsButBlobNotExists() + { + // Arrange + var now = DateTime.Now; + + var news = new NewsEntity + { + Title = "Test Title", + Text = "Sample text", + URL = "https://github.com/", + CreationDate = now, + Image = new Image + { + BlobName = "BlobName", + }, + }; + + var url = "https://github.com/"; + var request = new GetNewsByUrlQuery(url); + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false)) + .ReturnsAsync(news); + + // Act + var res = await _handler.Handle(request, CancellationToken.None); + + // Assert + res.IsFailed.Should().BeTrue(); + res.Errors.Should().ContainSingle( + Messages.Error_MediaBlobNotFound.Format(nameof(Image), news.Image.BlobName)); + } } } diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/News/SortedByDateTimeHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/News/SortedByDateTimeHandlerTests.cs index 2fe940f..0da9df7 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/News/SortedByDateTimeHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/News/SortedByDateTimeHandlerTests.cs @@ -92,7 +92,7 @@ public async Task Handle_ShouldReturnSortedNewsDtosWithImage_WhenNewsExists() .ReturnsAsync(allNews); _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) - .Returns(expectedBase64); + .ReturnsAsync(expectedBase64); var request = new SortedByDateTimeQuery(); @@ -142,5 +142,42 @@ public async Task Handle_ShouldReturnSortedNewsDtosWithOutImage_WhenNewsExists() Times.Never); res.Value.Should().BeInDescendingOrder(x => x.CreationDate); } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewsFoundWithImageButBlobNotExists() + { + // Arrange + var newsWithImage = new NewsEntity + { + Id = 1, + URL = "url1", + CreationDate = DateTime.Now, + Image = new Image + { + BlobName = "BlobName", + }, + }; + + _repositoryWrapperMock.Setup(r => r.NewsRepository.GetAllAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + false + )) + .ReturnsAsync(new List { newsWithImage }); + + _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync((string)null); + + var request = new SortedByDateTimeQuery(); + + // Act + var res = await _handler.Handle(request, CancellationToken.None); + + // Assert + res.IsFailed.Should().BeTrue(); + res.Errors.Should().ContainSingle(Messages.Error_MediaBlobNotFound.Format( + nameof(Image), + newsWithImage.Image.BlobName)); + } } } diff --git a/Streetcode/Streetcode.XUnitTest/MediatR/News/UpdateNewsHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/MediatR/News/UpdateNewsHandlerTests.cs index 49e5da1..faccbcf 100644 --- a/Streetcode/Streetcode.XUnitTest/MediatR/News/UpdateNewsHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/MediatR/News/UpdateNewsHandlerTests.cs @@ -11,10 +11,12 @@ using Streetcode.DAL.Entities.Media.Images; using Streetcode.DAL.Repositories.Interfaces.Base; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; using Streetcode.Resources; using Streetcode.Shared.Extensions; using Xunit; using NewsEntity = Streetcode.DAL.Entities.News.News; +using FactEntity = Streetcode.DAL.Entities.Streetcode.TextContent.Fact; namespace Streetcode.XUnitTest.MediatR.News { @@ -48,7 +50,7 @@ public UpdateNewsHandlerTests() } [Fact] - public async Task Handle_ShouldReturnFail_WhenNoNews() + public async Task Handle_ShouldReturnFail_WhenDTOIsNull() { // Arrange var req = new UpdateNewsCommand(null); @@ -63,7 +65,7 @@ public async Task Handle_ShouldReturnFail_WhenNoNews() } [Fact] - public async Task Handle_ShouldReturnFail_WhenCouldntUpdateNews() + public async Task Handle_ShouldReturnFail_WhenNewsWithIdNotFound() { // Arrange var newsDto = new NewsDTO @@ -73,16 +75,65 @@ public async Task Handle_ShouldReturnFail_WhenCouldntUpdateNews() ImageId = 0, }; - _repositoryWrapperMock.Setup(repo => repo.SaveChangesAsync()) - .ReturnsAsync(0); + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + null, + false)) + .ReturnsAsync((NewsEntity)null); - _repositoryWrapperMock.Setup(repo => repo.NewsRepository.Update(It.IsAny())); + var req = new UpdateNewsCommand(newsDto); + + // Act + var res = await _handler.Handle(req, CancellationToken.None); + + // Assert + res.IsFailed.Should().BeTrue(); + res.Errors.Should().ContainSingle(Messages.Error_EntityWithIdNotFound.Format( + nameof(DAL.Entities.News.News), newsDto.Id)); + } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenCouldntUpdateNews() + { + // Arrange + var fakeBase = "fake_base_64"; + var newsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 1, + Image = new ImageDTO + { + Id = 1, + }, + }; + + var news = new NewsEntity + { + Id = 1, + URL = "url", + ImageId = 1, + }; + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(news); _repositoryWrapperMock.Setup(repo => repo.ImageRepository.GetFirstOrDefaultAsync( - It.IsAny>>(), - null, - false)) - .ReturnsAsync((Image)null); + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync((Image)null!); + + _blobServiceMock.Setup(s => s.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync(fakeBase); + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.Update(It.IsAny())); + + _repositoryWrapperMock.Setup(repo => repo.SaveChangesAsync()) + .ReturnsAsync(0); var req = new UpdateNewsCommand(newsDto); @@ -95,37 +146,288 @@ public async Task Handle_ShouldReturnFail_WhenCouldntUpdateNews() repo => repo.NewsRepository.Update(It.IsAny()), Times.Once()); + _repositoryWrapperMock.Verify( + repo => repo.ImageRepository.Delete(It.IsAny()), + Times.Never); + res.Errors.Should().ContainSingle(Messages.Error_FailedToUpdateEntity.Format( nameof(DAL.Entities.News.News))); } [Fact] - public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsAndImageExist() + public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewImageExists() { // Arrange - var fakeBase = "fake_base64"; - + var fakeBase = "fake_base_64"; var newsDto = new NewsDTO { Id = 1, URL = "url1", - ImageId = 0, - Image = new ImageDTO(), + ImageId = 2, + Image = new ImageDTO + { + Id = 2, + }, }; - _blobServiceMock.Setup(bs => bs.FindFileInStorageAsBase64(It.IsAny())) - .Returns(fakeBase); + var expectedNewsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 2, + Image = new ImageDTO + { + Id = 2, + Base64 = fakeBase, + }, + }; - _repositoryWrapperMock.Setup(repo => repo.SaveChangesAsync()) + var news = new NewsEntity + { + Id = 1, + URL = "url", + ImageId = 1, + Image = new Image + { + Id = 1, + }, + }; + + var newImage = new Image { Id = 2 }; + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(news); + + _repositoryWrapperMock.Setup(repo => repo.ImageRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(newImage); + + _repositoryWrapperMock.Setup(repo => repo.FactRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync((FactEntity)null!); + + _blobServiceMock.Setup(s => s.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync(fakeBase); + + _blobServiceMock.Setup(s => s.DeleteFileInStorage(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.ImageRepository.Delete(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.NewsRepository.Update(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.SaveChangesAsync()) .ReturnsAsync(1); - _repositoryWrapperMock.Setup(repo => repo.NewsRepository.Update(It.IsAny())); + var req = new UpdateNewsCommand(newsDto); + + // Act + var res = await _handler.Handle(req, CancellationToken.None); + + // Assert + res.IsSuccess.Should().BeTrue(); + _repositoryWrapperMock.Verify( + repo => repo.NewsRepository.Update(It.IsAny()), + Times.Once()); + + _repositoryWrapperMock.Verify( + repo => repo.ImageRepository.Delete(It.IsAny()), + Times.Once()); + + _blobServiceMock.Verify( + b => b.DeleteFileInStorage(It.IsAny()), + Times.Once()); + + res.Value.Should().BeEquivalentTo(expectedNewsDto); + } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewImageNotExists() + { + // Arrange + var newsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 2, + Image = new ImageDTO + { + Id = 2, + }, + }; + + var news = new NewsEntity + { + Id = 1, + URL = "url", + ImageId = 1, + Image = new Image + { + Id = 1, + }, + }; + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(news); + + _repositoryWrapperMock.Setup(repo => repo.ImageRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync((Image)null!); + + var req = new UpdateNewsCommand(newsDto); + + // Act + var res = await _handler.Handle(req, CancellationToken.None); + + // Assert + res.IsFailed.Should().BeTrue(); + res.Errors.Should().ContainSingle(Messages.Error_EntityWithIdNotFound.Format( + nameof(Image), + newsDto.ImageId)); + } + + [Fact] + public async Task Handle_ShouldReturnFail_WhenNewImageExistsButBlobNotExists() + { + // Arrange + var newsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 2, + Image = new ImageDTO + { + Id = 2, + }, + }; + + var news = new NewsEntity + { + Id = 1, + URL = "url", + ImageId = 1, + Image = new Image + { + Id = 1, + }, + }; + + var newImage = new Image + { + Id = 2, + BlobName = "BlobName", + }; + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(news); + + _repositoryWrapperMock.Setup(repo => repo.ImageRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(newImage); + + _blobServiceMock.Setup(s => s.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync((string?)null); var req = new UpdateNewsCommand(newsDto); // Act var res = await _handler.Handle(req, CancellationToken.None); + // Assert + res.IsFailed.Should().BeTrue(); + res.Errors.Should().ContainSingle(Messages.Error_MediaBlobNotFound.Format( + nameof(Image), + newImage.BlobName)); + } + + [Fact] + public async Task Handle_ShouldDeleteOldImage_WhenNewImageIsNullAndFactNotLinkedToImage() + { + // Arrange + var fakeBase = "fake_base_64"; + var newsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 2, + }; + + var expectedNewsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 2, + }; + + var news = new NewsEntity + { + Id = 1, + URL = "url", + ImageId = 1, + Image = new Image + { + Id = 1, + }, + }; + + var newImage = new Image { Id = 2 }; + + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(news); + + _repositoryWrapperMock.Setup(repo => repo.ImageRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(newImage); + + _repositoryWrapperMock.Setup(repo => repo.FactRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync((FactEntity)null!); + + _blobServiceMock.Setup(s => s.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync(fakeBase); + + _blobServiceMock.Setup(s => s.DeleteFileInStorage(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.ImageRepository.Delete(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.NewsRepository.Update(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.SaveChangesAsync()) + .ReturnsAsync(1); + + var req = new UpdateNewsCommand(newsDto); + + // Act + var res = await _handler.Handle(req, CancellationToken.None); // Assert res.IsSuccess.Should().BeTrue(); @@ -133,31 +435,87 @@ public async Task Handle_ShouldReturnNewsDtoWithImage_WhenNewsAndImageExist() repo => repo.NewsRepository.Update(It.IsAny()), Times.Once()); - res.Value.Image.Base64.Should().Be(fakeBase); - } + _repositoryWrapperMock.Verify( + repo => repo.ImageRepository.Delete(It.IsAny()), + Times.Once()); + _blobServiceMock.Verify( + b => b.DeleteFileInStorage(It.IsAny()), + Times.Once()); + + res.Value.Should().BeEquivalentTo(expectedNewsDto); + } + [Fact] - public async Task Handle_ShouldDeleteOldImage_WhenNewImageIsNull_AndOldImageExists() + public async Task Handle_ShouldNotDeleteOldImage_WhenNewImageIsNullAndFactLinkedToImage() { // Arrange + var fakeBase = "fake_base_64"; var newsDto = new NewsDTO { Id = 1, URL = "url1", - ImageId = 10, - Image = null, + ImageId = 2, + }; + + var expectedNewsDto = new NewsDTO + { + Id = 1, + URL = "url1", + ImageId = 2, + }; + + var news = new NewsEntity + { + Id = 1, + URL = "url", + ImageId = 1, + Image = new Image + { + Id = 1, + }, + }; + + var newImage = new Image { Id = 2 }; + + var fact = new FactEntity + { + Id = 1, + ImageId = 1, }; - var oldImage = new Image { Id = 10 }; + _repositoryWrapperMock.Setup(repo => repo.NewsRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(news); _repositoryWrapperMock.Setup(repo => repo.ImageRepository.GetFirstOrDefaultAsync( - It.IsAny>>(), - null, - false)) - .ReturnsAsync(oldImage); + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(newImage); - _repositoryWrapperMock.Setup(repo => repo.NewsRepository.Update(It.IsAny())); - _repositoryWrapperMock.Setup(repo => repo.SaveChangesAsync()).ReturnsAsync(1); + _repositoryWrapperMock.Setup(repo => repo.FactRepository.GetFirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(fact); + + _blobServiceMock.Setup(s => s.FindFileInStorageAsBase64(It.IsAny())) + .ReturnsAsync(fakeBase); + + _blobServiceMock.Setup(s => s.DeleteFileInStorage(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.ImageRepository.Delete(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.NewsRepository.Update(It.IsAny())); + + _repositoryWrapperMock + .Setup(repo => repo.SaveChangesAsync()) + .ReturnsAsync(1); var req = new UpdateNewsCommand(newsDto); @@ -166,12 +524,19 @@ public async Task Handle_ShouldDeleteOldImage_WhenNewImageIsNull_AndOldImageExis // Assert res.IsSuccess.Should().BeTrue(); + _repositoryWrapperMock.Verify( + repo => repo.NewsRepository.Update(It.IsAny()), + Times.Once()); _repositoryWrapperMock.Verify( - repo => repo.ImageRepository.Delete(oldImage), - Times.Once); + repo => repo.ImageRepository.Delete(It.IsAny()), + Times.Never); + + _blobServiceMock.Verify( + b => b.DeleteFileInStorage(It.IsAny()), + Times.Never); - _repositoryWrapperMock.Verify(repo => repo.NewsRepository.Update(It.IsAny()), Times.Once); + res.Value.Should().BeEquivalentTo(expectedNewsDto); } } } diff --git a/Streetcode/Streetcode.XUnitTest/Services/AzureBlobServiceTests.cs b/Streetcode/Streetcode.XUnitTest/Services/AzureBlobServiceTests.cs new file mode 100644 index 0000000..3c5d0e4 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/Services/AzureBlobServiceTests.cs @@ -0,0 +1,266 @@ +using System.Linq.Expressions; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.BLL.Services.BlobStorageService; +using Streetcode.DAL.Repositories.Interfaces.Base; +using Streetcode.Shared.Services; +using Azure.Storage.Blobs.Models; +using Azure; +using Azure.Storage.Blobs; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.Extensions.Options; +using Moq; +using Streetcode.DAL.Entities.Media; +using Streetcode.DAL.Entities.Media.Images; +using Streetcode.Shared.Extensions; +using Xunit; + +namespace Streetcode.XUnitTest.Services.AzureBlobService; + +public class AzureBlobServiceTests +{ + private readonly Mock _repositoryWrapperMock; + private readonly Mock _blobServiceClientMock; + private readonly Mock _containerClientMock; + private readonly Mock _blobClientMock; + private readonly Mock _loggerMock; + private readonly IOptions _options; + private readonly BLL.Services.BlobStorageService.AzureBlobService _service; + + public AzureBlobServiceTests() + { + _repositoryWrapperMock = new Mock(); + _blobServiceClientMock = new Mock(); + _containerClientMock = new Mock(); + _blobClientMock = new Mock(); + _loggerMock = new Mock(); + + var envVars = new AzureBlobEnvironmentVariables + { + ContainerName = "test-container", + EncryptionKey = "test-key-12345678901234567890-32", // Ensure 32 bytes(characters) + }; + _options = Options.Create(envVars); + + _blobServiceClientMock + .Setup(x => x.GetBlobContainerClient(It.IsAny())) + .Returns(_containerClientMock.Object); + + _containerClientMock + .Setup(x => x.GetBlobClient(It.IsAny())) + .Returns(_blobClientMock.Object); + + _service = new BLL.Services.BlobStorageService.AzureBlobService( + _options, + _repositoryWrapperMock.Object, + _blobServiceClientMock.Object, + _loggerMock.Object); + } + + [Fact] + public async Task SaveFileInStorage_ShouldReturnHashNameAndUploadBlob() + { + // Arrange + string base64 = Convert.ToBase64String(new byte[] { 1, 2, 3 }); + string name = "testfile"; + string extension = "jpg"; + + // Act + var result = await _service.SaveFileInStorage(base64, name, extension); + + // Assert + Assert.False(string.IsNullOrEmpty(result)); + _containerClientMock.Verify( + x => x.CreateIfNotExistsAsync(default,default, default, default), + Times.Once); + + _blobClientMock.Verify( + x => x.UploadAsync(It.IsAny(), It.IsAny(), default), + Times.Once); + } + + [Fact] + public async Task SaveFileInStorage_ShouldEncryptBeforeUploading() + { + // Arrange + var rawData = new byte[] { 7, 8, 9 }; + var base64 = Convert.ToBase64String(rawData); + byte[]? uploadedBytes = null; + + _blobClientMock + .Setup(x => x.UploadAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((s, _, _) => + { + using var ms = new MemoryStream(); + s.CopyTo(ms); + uploadedBytes = ms.ToArray(); + }) + .ReturnsAsync(Mock.Of>()); + + // Act + await _service.SaveFileInStorage(base64, "testfile", "png"); + + // Assert + Assert.NotNull(uploadedBytes); + // Verify it's actually encrypted (the uploaded size should be IV(16) + EncryptedData) + Assert.NotEqual(rawData, uploadedBytes); + Assert.True(uploadedBytes.Length > rawData.Length); + } + + [Fact] + public async Task DeleteFileInStorage_ShouldCallDeleteIfExists() + { + // Arrange + string fileName = "test-blob-name"; + + // Act + await _service.DeleteFileInStorage(fileName); + + // Assert + _blobClientMock.Verify( + x => x.DeleteIfExistsAsync(default, default, default), + Times.Once); + + _loggerMock.Verify( + x => x.LogInformation(It.Is(s => s.Contains(fileName))), + Times.Once); + } + + [Fact] + public async Task FindFileInStorageAsBase64_ReturnNull_WhenBlobNotFound() + { + // Arrange + _blobClientMock + .Setup(x => x.DownloadToAsync(It.IsAny())) + .Throws(new RequestFailedException(404, "Not Found")); + + // Act + var result = await _service.FindFileInStorageAsBase64("non-existent"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task FindFileInStorageAsMemoryStream_ReturnNull_WhenBlobNotFound() + { + // Arrange + _blobClientMock + .Setup(x => x.DownloadToAsync(It.IsAny())) + .Throws(new RequestFailedException(404, "Not Found")); + + // Act + var result = await _service.FindFileInStorageAsMemoryStream("non-existent"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task FindFileInStorageAsBase64_ReturnsOriginalString_WhenBlobExists() + { + // Arrange + var originalBytes = new byte[] { 1, 2, 3, 4, 5 }; + var base64Input = Convert.ToBase64String(originalBytes); + + var encryptedDataWithIv = FileService.EncryptBytes(originalBytes, _options.Value.EncryptionKey); + + _blobClientMock + .Setup(x => x.DownloadToAsync(It.IsAny())) + .Callback(s => s.Write(encryptedDataWithIv, 0, encryptedDataWithIv.Length)) + .ReturnsAsync(Mock.Of()); + + // Act + var result = await _service.FindFileInStorageAsBase64("some-blob-name"); + + // Assert + Assert.NotNull(result); + Assert.Equal(base64Input, result); + } + + [Fact] + public async Task FindFileInStorageAsMemoryStream_ReturnsDecryptedStream_WhenBlobExists() + { + // Arrange + var originalBytes = new byte[] { 10, 20, 30, 40, 50 }; + var key = _options.Value.EncryptionKey; + var encryptedDataWithIv = FileService.EncryptBytes(originalBytes, key); + + _blobClientMock + .Setup(x => x.DownloadToAsync(It.IsAny())) + .Callback(s => s.Write(encryptedDataWithIv, 0, encryptedDataWithIv.Length)) + .ReturnsAsync(Mock.Of()); + + // Act + using var resultStream = await _service.FindFileInStorageAsMemoryStream("some-blob-name"); + + // Assert + Assert.NotNull(resultStream); + + byte[] actualBytes = resultStream.ToArray(); + + Assert.Equal(originalBytes.Length, actualBytes.Length); + Assert.Equal(originalBytes, actualBytes); + } + + [Fact] + public async Task UpdateFileInStorage_ShouldDeleteOldAndUploadNew() + { + // Arrange + string oldName = "old.png"; + string base64 = Convert.ToBase64String(new byte[] { 4, 5, 6 }); + + // Act + await _service.UpdateFileInStorage(oldName, base64, "newFile", "png"); + + // Assert + _blobClientMock.Verify( + x => x.DeleteIfExistsAsync(default, default, default), + Times.Once); + + _blobClientMock.Verify( + x => x.UploadAsync(It.IsAny(), true, default), + Times.AtLeastOnce); + } + + [Fact] + public async Task CleanBlobStorage_Azure_ShouldBatchDeleteOrphans() + { + // Arrange + var blobName1 = "db_image.png"; + var blobName2 = "orphan.png"; + + var blobItems = new List + { + BlobsModelFactory.BlobItem(blobName1), + BlobsModelFactory.BlobItem(blobName2), + }; + + var mockAsyncPageable = new Mock>(); + mockAsyncPageable.Setup(x => x.GetAsyncEnumerator(default)) + .Returns(blobItems.ToAsyncEnumerable().GetAsyncEnumerator(CancellationToken.None)); + + _containerClientMock.Setup(x => x.GetBlobsAsync(default, default, default, default)) + .Returns(mockAsyncPageable.Object); + + _repositoryWrapperMock.Setup(r => r.ImageRepository.GetAllAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { new () { BlobName = blobName1 } }); + + _repositoryWrapperMock + .Setup(r => r.AudioRepository.GetAllAsync( + It.IsAny>>(), + It.IsAny, IIncludableQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List