edd7389d7d
- IJobItemAssemblyService / IQuotePricingAssemblyService: centralize job item and quote pricing construction that was duplicated across create, rework copy, and quote-to-job conversion paths - BlobFileHelper: single ValidateUpload/GetContentType/SanitizeFileName used by 6 blob services (JobPhoto, QuotePhoto, ProfilePhoto, CompanyLogo, Equipment, Catalog) and BillsController + ExpensesController, removing 8 private copies - PagedResult<T>.From(): static factory eliminates 6-line boilerplate in 11 controllers (Appointments, Customers, Equipment, Inventory, Invoices, Jobs, Maintenance, CompanyUsers, PlatformUsers, Quotes, Vendors) - AccountingDropdownHelper: single LoadAsync() call replaces duplicate vendor/account/job queries in BillsController and ExpensesController - JobTemplateItem: add IsSalesItem + Sku fields with migration; propagate through JobTemplatesController snapshot copy and GetTemplatesJson projection, and JobsController template-application path - Test assertions updated for standardized BlobFileHelper error messages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
5.5 KiB
C#
174 lines
5.5 KiB
C#
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 provided.", 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("File exceeds the 10 MB limit.", 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("File type not allowed. Allowed: .jpg, .jpeg, .png, .gif, .webp.", result.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveJobPhotoAsync_ReturnsBlobError_WhenUploadFails()
|
|
{
|
|
var blobService = new Mock<IAzureBlobStorageService>();
|
|
blobService
|
|
.Setup(x => x.UploadAsync("jobimages", It.IsAny<string>(), It.IsAny<Stream>(), "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<IAzureBlobStorageService>();
|
|
blobService
|
|
.Setup(x => x.UploadAsync("jobimages", It.IsAny<string>(), It.IsAny<Stream>(), "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<IAzureBlobStorageService>();
|
|
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<IAzureBlobStorageService>();
|
|
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<IAzureBlobStorageService>? blobService = null)
|
|
{
|
|
var settings = Options.Create(new StorageSettings
|
|
{
|
|
Containers = new StorageContainers
|
|
{
|
|
JobImages = "jobimages"
|
|
}
|
|
});
|
|
|
|
return new JobPhotoService(
|
|
(blobService ?? new Mock<IAzureBlobStorageService>()).Object,
|
|
settings,
|
|
Mock.Of<ILogger<JobPhotoService>>());
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|