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; using PowderCoating.Core.Enums; namespace PowderCoating.UnitTests; public class JobPhotoServiceTests { [Fact] public async Task SaveJobPhotoAsync_ReturnsError_WhenFileMissing() { var service = CreateService(); var result = await service.SaveJobPhotoAsync(null!, 1, 2); Assert.False(result.Success); Assert.Equal("No file was uploaded.", result.ErrorMessage); } [Fact] public async Task SaveJobPhotoAsync_ReturnsError_WhenFileTooLarge() { var service = CreateService(); var file = CreateFormFile("big.jpg", 10 * 1024 * 1024 + 1); var result = await service.SaveJobPhotoAsync(file, 1, 2); Assert.False(result.Success); Assert.Equal("Photo must be smaller than 10 MB.", result.ErrorMessage); } [Fact] public async Task SaveJobPhotoAsync_ReturnsError_WhenExtensionNotAllowed() { var service = CreateService(); var file = CreateFormFile("notes.txt"); var result = await service.SaveJobPhotoAsync(file, 1, 2); Assert.False(result.Success); Assert.Equal("Only JPG, PNG, GIF, and WebP images are allowed.", result.ErrorMessage); } [Fact] public async Task SaveJobPhotoAsync_ReturnsBlobError_WhenUploadFails() { var blobService = new Mock(); blobService .Setup(x => x.UploadAsync("jobimages", It.IsAny(), It.IsAny(), "image/png")) .ReturnsAsync((false, "upload failed")); var service = CreateService(blobService); var result = await service.SaveJobPhotoAsync(CreateFormFile("photo.png"), 9, 7); Assert.False(result.Success); Assert.Equal("upload failed", result.ErrorMessage); } [Fact] public async Task SaveJobPhotoAsync_UsesTenantScopedBlobPath_WhenSuccessful() { var blobService = new Mock(); blobService .Setup(x => x.UploadAsync("jobimages", It.IsAny(), It.IsAny(), "image/webp")) .ReturnsAsync((true, string.Empty)); var service = CreateService(blobService); var result = await service.SaveJobPhotoAsync(CreateFormFile("photo.webp"), 9, 7, "caption", JobPhotoType.After); Assert.True(result.Success); Assert.StartsWith("7/job-photos/9/", result.FilePath); Assert.EndsWith(".webp", result.FilePath); } [Fact] public async Task DeleteJobPhotoAsync_ReturnsError_WhenPathMissing() { var service = CreateService(); var result = await service.DeleteJobPhotoAsync(string.Empty); Assert.False(result.Success); Assert.Equal("File path is required.", result.ErrorMessage); } [Fact] public async Task GetJobPhotoAsync_ReturnsError_WhenPathMissing() { var service = CreateService(); var result = await service.GetJobPhotoAsync(" "); Assert.False(result.Success); Assert.Equal("File path is required.", result.ErrorMessage); Assert.Empty(result.FileContent); } [Fact] public async Task JobPhotoExistsAsync_ReturnsFalse_WhenPathMissing() { var service = CreateService(); var result = await service.JobPhotoExistsAsync(null!); Assert.False(result); } [Fact] public async Task GetJobPhotoAsync_ProxiesBlobDownload() { var blobService = new Mock(); blobService .Setup(x => x.DownloadAsync("jobimages", "7/job-photos/9/photo.jpg")) .ReturnsAsync((true, new byte[] { 1, 2 }, "image/jpeg", string.Empty)); var service = CreateService(blobService); var result = await service.GetJobPhotoAsync("7/job-photos/9/photo.jpg"); Assert.True(result.Success); Assert.Equal("image/jpeg", result.ContentType); Assert.Equal(new byte[] { 1, 2 }, result.FileContent); } [Fact] public async Task JobPhotoExistsAsync_UsesBlobServiceForValidPath() { var blobService = new Mock(); blobService .Setup(x => x.ExistsAsync("jobimages", "7/job-photos/9/photo.jpg")) .ReturnsAsync(true); var service = CreateService(blobService); var result = await service.JobPhotoExistsAsync("7/job-photos/9/photo.jpg"); Assert.True(result); } private static JobPhotoService CreateService(Mock? blobService = null) { var settings = Options.Create(new StorageSettings { Containers = new StorageContainers { JobImages = "jobimages" } }); return new JobPhotoService( (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); } }