Files
PowderCoatingLogix/tests/PowderCoating.UnitTests/JobPhotoServiceTests.cs
T
spouliot edd7389d7d Refactor: extract shared helpers, fix field drift, add assembly services
- 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>
2026-05-09 22:12:33 -04:00

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);
}
}