Files
PowderCoatingLogix/tests/PowderCoating.UnitTests/DepositsControllerTests.cs
T
spouliot 71caa93461 Fix unit test build failures after logo service and pricing changes
DepositsController and GiftCertificatesController gained a required
ICompanyLogoService constructor parameter in the PDF logo fix; their
test factories were not updated and failed to compile on Jenkins.
Added Mock.Of<ICompanyLogoService>() to both factory methods and the
missing using directive to DepositsControllerTests.

PricingCalculationService now only charges oven cost for items that
have explicit coating layers (Coats collection non-empty), because
sandblast/prep-only and labor items do not go in the oven. Two tests
that tested the old "all items count toward oven fraction" logic were
updated to include a single coat entry on each item, which restores
the expected oven fraction math without changing the tested behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:38:18 -04:00

357 lines
14 KiB
C#

using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Infrastructure.Repositories;
using PowderCoating.Web.Controllers;
namespace PowderCoating.UnitTests;
public class DepositsControllerTests
{
// ── Record — input validation (no user resolution needed) ─────────────
[Fact]
public async Task Record_WhenAmountIsZero_ReturnsJsonError()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: 0m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Contains("greater than zero", doc.RootElement.GetProperty("message").GetString());
}
[Fact]
public async Task Record_WhenAmountIsNegative_ReturnsJsonError()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: -50m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
}
[Fact]
public async Task Record_WhenNeitherJobNorQuoteProvided_ReturnsJsonError()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Record(jobId: null, quoteId: null, customerId: 1,
amount: 100m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Contains("Job or Quote must be specified", doc.RootElement.GetProperty("message").GetString());
}
[Fact]
public async Task Record_WhenInvalidPaymentMethod_ReturnsJsonError()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: 100m, paymentMethod: "BitcoinPizza",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Contains("Invalid payment method", doc.RootElement.GetProperty("message").GetString());
}
[Fact]
public async Task Record_WhenUserNotFound_ReturnsUnauthorized()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: null);
var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: 100m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
Assert.IsType<UnauthorizedResult>(result);
}
// ── Record — success paths ────────────────────────────────────────────
[Fact]
public async Task Record_WithJobId_CreatesDepositAndReturnsSuccessJson()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(companyId: 1));
var receivedDate = new DateTime(2026, 4, 15);
var result = await controller.Record(jobId: 42, quoteId: null, customerId: 7,
amount: 250m, paymentMethod: "Check",
receivedDate: receivedDate, reference: "REF-001", notes: "Half up front");
using var doc = ParseJson(result);
Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Equal(250m, doc.RootElement.GetProperty("amount").GetDecimal());
Assert.Equal("Check", doc.RootElement.GetProperty("paymentMethod").GetString());
Assert.Equal("04/15/2026", doc.RootElement.GetProperty("receivedDate").GetString());
Assert.Equal("REF-001", doc.RootElement.GetProperty("reference").GetString());
Assert.Equal("Half up front", doc.RootElement.GetProperty("notes").GetString());
var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync();
Assert.Equal(42, deposit.JobId);
Assert.Null(deposit.QuoteId);
Assert.Equal(7, deposit.CustomerId);
Assert.Equal(250m, deposit.Amount);
Assert.Equal(PaymentMethod.Check, deposit.PaymentMethod);
}
[Fact]
public async Task Record_WithQuoteId_CreatesDepositLinkedToQuote()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(companyId: 1));
var result = await controller.Record(jobId: null, quoteId: 99, customerId: 5,
amount: 500m, paymentMethod: "CreditDebitCard",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Equal("Credit / Debit Card", doc.RootElement.GetProperty("paymentMethod").GetString());
var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync();
Assert.Equal(99, deposit.QuoteId);
Assert.Null(deposit.JobId);
}
[Fact]
public async Task Record_GeneratesReceiptNumberInExpectedFormat()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(companyId: 1));
var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: 100m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
var receiptNumber = doc.RootElement.GetProperty("receiptNumber").GetString()!;
// Format: DEP-YYMM-#### (e.g. DEP-2604-0001)
Assert.Matches(@"^DEP-\d{4}-\d{4}$", receiptNumber);
Assert.EndsWith("-0001", receiptNumber);
}
[Fact]
public async Task Record_SecondDepositInSameMonth_IncrementsSequenceNumber()
{
await using var context = CreateContext();
var user = CreateUser(companyId: 1);
var controller = CreateController(context, currentUser: user);
// First deposit
await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: 100m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
// Second deposit
var result = await controller.Record(jobId: 2, quoteId: null, customerId: 1,
amount: 200m, paymentMethod: "Cash",
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
var receiptNumber = doc.RootElement.GetProperty("receiptNumber").GetString()!;
Assert.EndsWith("-0002", receiptNumber);
}
[Fact]
public async Task Record_PaymentMethodDisplayStrings_AreHumanReadable()
{
await using var context = CreateContext();
var user = CreateUser(companyId: 1);
var cases = new[]
{
("Cash", "Cash"),
("Check", "Check"),
("CreditDebitCard", "Credit / Debit Card"),
("BankTransferACH", "Bank Transfer / ACH"),
("DigitalPayment", "Digital Payment"),
};
foreach (var (method, expected) in cases)
{
var controller = CreateController(context, currentUser: user);
var result = await controller.Record(jobId: 1, quoteId: null, customerId: 1,
amount: 10m, paymentMethod: method,
receivedDate: DateTime.UtcNow, reference: null, notes: null);
using var doc = ParseJson(result);
if (doc.RootElement.GetProperty("success").GetBoolean())
Assert.Equal(expected, doc.RootElement.GetProperty("paymentMethod").GetString());
}
}
// ── Delete ────────────────────────────────────────────────────────────
[Fact]
public async Task Delete_WhenDepositNotFound_ReturnsJsonError()
{
await using var context = CreateContext();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Delete(id: 999, returnUrl: null);
using var doc = ParseJson(result);
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Contains("not found", doc.RootElement.GetProperty("message").GetString());
}
[Fact]
public async Task Delete_WhenDepositAlreadyAppliedToInvoice_ReturnsJsonError()
{
await using var context = CreateContext();
context.Deposits.Add(new Deposit
{
Id = 1,
CompanyId = 1,
ReceiptNumber = "DEP-2604-0001",
CustomerId = 1,
JobId = 1,
Amount = 100m,
PaymentMethod = PaymentMethod.Cash,
ReceivedDate = DateTime.UtcNow,
AppliedToInvoiceId = 55 // already applied
});
await context.SaveChangesAsync();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Delete(id: 1, returnUrl: null);
using var doc = ParseJson(result);
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
Assert.Contains("already been applied", doc.RootElement.GetProperty("message").GetString());
}
[Fact]
public async Task Delete_WhenUnappliedDeposit_SoftDeletesAndReturnsSuccess()
{
await using var context = CreateContext();
context.Deposits.Add(new Deposit
{
Id = 2,
CompanyId = 1,
ReceiptNumber = "DEP-2604-0001",
CustomerId = 1,
JobId = 1,
Amount = 75m,
PaymentMethod = PaymentMethod.Cash,
ReceivedDate = DateTime.UtcNow,
AppliedToInvoiceId = null // not yet applied
});
await context.SaveChangesAsync();
var controller = CreateController(context, currentUser: CreateUser(1));
var result = await controller.Delete(id: 2, returnUrl: null);
using var doc = ParseJson(result);
Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
var deposit = await context.Deposits.IgnoreQueryFilters().SingleAsync(d => d.Id == 2);
Assert.True(deposit.IsDeleted);
}
// ── Helpers ───────────────────────────────────────────────────────────
private static DepositsController CreateController(
ApplicationDbContext context,
ApplicationUser? currentUser)
{
var uow = new UnitOfWork(context);
var userManager = CreateUserManagerMock();
userManager
.Setup(m => m.GetUserAsync(It.IsAny<ClaimsPrincipal>()))
.ReturnsAsync(currentUser);
var controller = new DepositsController(
uow,
userManager.Object,
Mock.Of<ILogger<DepositsController>>(),
Mock.Of<ICompanyLogoService>());
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
};
return controller;
}
private static ApplicationUser CreateUser(int companyId) => new()
{
Id = "test-user",
CompanyId = companyId,
UserName = "test@example.com",
Email = "test@example.com",
FirstName = "Test",
LastName = "User"
};
private static Mock<UserManager<ApplicationUser>> CreateUserManagerMock()
{
var store = new Mock<IUserStore<ApplicationUser>>();
return new Mock<UserManager<ApplicationUser>>(
store.Object, null!, null!, null!, null!, null!, null!, null!, null!);
}
private static JsonDocument ParseJson(IActionResult result)
{
var json = Assert.IsType<JsonResult>(result);
var serialized = JsonSerializer.Serialize(json.Value);
return JsonDocument.Parse(serialized);
}
private static ApplicationDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
// Use a SuperAdmin user (no CompanyId claim) so that IsPlatformAdmin = true and the
// global query filter evaluates to !IsDeleted only — not !IsDeleted && CompanyId == null,
// which would filter out every row. DefaultHttpContext.Session throws, so we mock
// HttpContext itself and stub Session.TryGetValue to return false (no impersonation).
var identity = new ClaimsIdentity(
new[] { new Claim(ClaimTypes.Role, "SuperAdmin") }, "Test");
var principal = new ClaimsPrincipal(identity);
byte[]? noBytes = null;
var sessionMock = new Mock<ISession>();
sessionMock.Setup(s => s.TryGetValue(It.IsAny<string>(), out noBytes)).Returns(false);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.SetupGet(c => c.User).Returns(principal);
httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object);
var accessor = new Mock<IHttpContextAccessor>();
accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object);
return new ApplicationDbContext(options, accessor.Object, null!);
}
}