using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using PowderCoating.Application.Configuration; using PowderCoating.Application.Interfaces; using PowderCoating.Application.Services; namespace PowderCoating.UnitTests; public class QuotePhotoServiceTests { [Fact] public async Task SaveTempPhotoAsync_ReturnsError_WhenFileMissing() { var service = CreateService(); var result = await service.SaveTempPhotoAsync(null!, companyId: 1); Assert.False(result.Success); Assert.Equal("No file provided.", result.ErrorMessage); } [Fact] public async Task SaveTempPhotoAsync_ReturnsError_WhenFileTooLarge() { var service = CreateService(); var file = CreateFormFile("huge.jpg", 10 * 1024 * 1024 + 1); var result = await service.SaveTempPhotoAsync(file, companyId: 1); Assert.False(result.Success); Assert.Equal("File exceeds the 10 MB limit.", result.ErrorMessage); } [Fact] public async Task SaveTempPhotoAsync_ReturnsError_WhenExtensionNotAllowed() { var service = CreateService(); var file = CreateFormFile("photo.bmp"); var result = await service.SaveTempPhotoAsync(file, companyId: 1); Assert.False(result.Success); Assert.Equal("File type '.bmp' is not allowed.", result.ErrorMessage); } [Fact] public async Task SaveTempPhotoAsync_ReturnsBlobError_WhenUploadFails() { var blobService = new Mock(); blobService .Setup(x => x.UploadAsync("quoteimages", It.IsAny(), It.IsAny(), "image/png")) .ReturnsAsync((false, "blob upload failed")); var service = CreateService(blobService: blobService); var file = CreateFormFile("photo.png"); var result = await service.SaveTempPhotoAsync(file, companyId: 1); Assert.False(result.Success); Assert.Equal("blob upload failed", result.ErrorMessage); } [Fact] public async Task SaveTempPhotoAsync_UploadsToTempPath_WhenValid() { var blobService = new Mock(); blobService .Setup(x => x.UploadAsync("quoteimages", It.IsAny(), It.IsAny(), "image/jpeg")) .ReturnsAsync((true, string.Empty)); var service = CreateService(blobService: blobService); var file = CreateFormFile("photo.jpg"); var result = await service.SaveTempPhotoAsync(file, companyId: 5); Assert.True(result.Success); Assert.False(string.IsNullOrWhiteSpace(result.TempId)); Assert.StartsWith($"temp/{result.TempId}/", result.FilePath); Assert.EndsWith(".jpg", result.FilePath); } [Fact] public async Task PromoteTempPhotoAsync_ReturnsError_WhenTempPhotoMissing() { var blobService = new Mock(); blobService .Setup(x => x.ListBlobsByPrefixAsync("quoteimages", "temp/temp123/")) .ReturnsAsync(Array.Empty()); var service = CreateService(blobService: blobService); var result = await service.PromoteTempPhotoAsync("temp123", quoteId: 10, companyId: 3); Assert.False(result.Success); Assert.Equal("Temp photo not found.", result.ErrorMessage); } [Fact] public async Task PromoteTempPhotoAsync_ReturnsError_WhenTempDownloadFails() { var blobService = new Mock(); blobService .Setup(x => x.ListBlobsByPrefixAsync("quoteimages", "temp/temp123/")) .ReturnsAsync(new[] { "temp/temp123/original.png" }); blobService .Setup(x => x.DownloadAsync("quoteimages", "temp/temp123/original.png")) .ReturnsAsync((false, Array.Empty(), string.Empty, "download failed")); var service = CreateService(blobService: blobService); var result = await service.PromoteTempPhotoAsync("temp123", quoteId: 10, companyId: 3); Assert.False(result.Success); Assert.Equal("Failed to read temp photo.", result.ErrorMessage); } [Fact] public async Task PromoteTempPhotoAsync_ReturnsError_WhenPermanentUploadFails() { var blobService = new Mock(); blobService .Setup(x => x.ListBlobsByPrefixAsync("quoteimages", "temp/temp123/")) .ReturnsAsync(new[] { "temp/temp123/original.webp" }); blobService .Setup(x => x.DownloadAsync("quoteimages", "temp/temp123/original.webp")) .ReturnsAsync((true, new byte[] { 1, 2, 3 }, "image/webp", string.Empty)); blobService .Setup(x => x.UploadAsync("quoteimages", It.IsAny(), It.IsAny(), "image/webp")) .ReturnsAsync((false, "upload failed")); var service = CreateService(blobService: blobService); var result = await service.PromoteTempPhotoAsync("temp123", quoteId: 10, companyId: 3); Assert.False(result.Success); Assert.Equal("Failed to save permanent photo.", result.ErrorMessage); } [Fact] public async Task PromoteTempPhotoAsync_PromotesAndDeletesTempBlob_WhenSuccessful() { var blobService = new Mock(); blobService .Setup(x => x.ListBlobsByPrefixAsync("quoteimages", "temp/temp123/")) .ReturnsAsync(new[] { "temp/temp123/original.png" }); blobService .Setup(x => x.DownloadAsync("quoteimages", "temp/temp123/original.png")) .ReturnsAsync((true, new byte[] { 1, 2, 3 }, "image/png", string.Empty)); blobService .Setup(x => x.UploadAsync("quoteimages", It.IsAny(), It.IsAny(), "image/png")) .ReturnsAsync((true, string.Empty)); blobService .Setup(x => x.DeleteAsync("quoteimages", "temp/temp123/original.png")) .ReturnsAsync((true, string.Empty)); var service = CreateService(blobService: blobService); var result = await service.PromoteTempPhotoAsync("temp123", quoteId: 10, companyId: 3); Assert.True(result.Success); Assert.StartsWith("3/quote-photos/10/", result.FilePath); Assert.EndsWith(".png", result.FilePath); blobService.Verify(x => x.DeleteAsync("quoteimages", "temp/temp123/original.png"), Times.Once); } [Fact] public async Task ReadTempPhotosAsync_ReturnsOnlySuccessfulDownloads() { var blobService = new Mock(); blobService .Setup(x => x.ListBlobsByPrefixAsync("quoteimages", "temp/temp123/")) .ReturnsAsync(new[] { "temp/temp123/one.jpg", "temp/temp123/two.jpg" }); blobService .Setup(x => x.DownloadAsync("quoteimages", "temp/temp123/one.jpg")) .ReturnsAsync((true, new byte[] { 1 }, "image/jpeg", string.Empty)); blobService .Setup(x => x.DownloadAsync("quoteimages", "temp/temp123/two.jpg")) .ReturnsAsync((false, Array.Empty(), string.Empty, "failed")); var service = CreateService(blobService: blobService); var result = await service.ReadTempPhotosAsync("temp123"); Assert.Single(result); Assert.Equal("one.jpg", result[0].FileName); Assert.Equal("image/jpeg", result[0].ContentType); } [Fact] public async Task CleanupTempAsync_ContinuesDeleting_WhenOneDeleteThrows() { var blobService = new Mock(); blobService .Setup(x => x.ListBlobsByPrefixAsync("quoteimages", "temp/temp123/")) .ReturnsAsync(new[] { "temp/temp123/one.jpg", "temp/temp123/two.jpg" }); blobService .Setup(x => x.DeleteAsync("quoteimages", "temp/temp123/one.jpg")) .ThrowsAsync(new InvalidOperationException("boom")); blobService .Setup(x => x.DeleteAsync("quoteimages", "temp/temp123/two.jpg")) .ReturnsAsync((true, string.Empty)); var service = CreateService(blobService: blobService); await service.CleanupTempAsync("temp123"); blobService.Verify(x => x.DeleteAsync("quoteimages", "temp/temp123/one.jpg"), Times.Once); blobService.Verify(x => x.DeleteAsync("quoteimages", "temp/temp123/two.jpg"), Times.Once); } private static QuotePhotoService CreateService(Mock? blobService = null) { var settings = Options.Create(new StorageSettings { Containers = new StorageContainers { QuoteImages = "quoteimages" } }); return new QuotePhotoService( (blobService ?? new Mock()).Object, settings, Mock.Of>()); } private static IFormFile CreateFormFile(string fileName, long? lengthOverride = null) { var dataLength = lengthOverride.HasValue ? (int)Math.Min(lengthOverride.Value, 1024) : 16; var bytes = Enumerable.Repeat((byte)65, dataLength).ToArray(); var stream = new MemoryStream(bytes); return new FormFile(stream, 0, lengthOverride ?? bytes.Length, "file", fileName); } }