Add unit tests for 9 new services/controllers and expand existing test coverage

116 tests passing: JobPhotoService, MeasurementConversionService, PlatformSettingsService,
QuoteApprovalController, QuotePhotoService, ShopCapabilityCalculator, StorageMigrationService,
TenantContext, UsageQuotaController — plus expanded PricingCalculation, Registration, and
Subscription tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 18:27:30 -04:00
parent edce8e8c4a
commit dbe4170986
13 changed files with 2930 additions and 12 deletions
@@ -1,13 +1,18 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using PowderCoating.Application.DTOs.Registration;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Shared.Constants;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Infrastructure.Repositories;
using System.Text.Json;
using PowderCoating.Web.Controllers;
using Xunit;
@@ -95,18 +100,315 @@ public class RegistrationControllerTests
Assert.True((await context.PendingRegistrationSessions.SingleAsync()).IsCompleted);
}
[Fact]
public async Task Create_WhenEmailAlreadyExists_ReturnsIndexWithModelError()
{
await using var context = CreateContext();
SeedPlanConfig(context, plan: 1);
await context.SaveChangesAsync();
var existingUser = new ApplicationUser
{
Id = "existing",
Email = "owner@example.com",
UserName = "owner@example.com",
FirstName = "Existing",
LastName = "User"
};
var userManager = CreateUserManagerMock();
userManager.Setup(x => x.FindByEmailAsync("owner@example.com")).ReturnsAsync(existingUser);
var controller = CreateController(context, userManager: userManager);
var model = new RegisterCompanyDto
{
CompanyName = "Dup Co",
CompanyPhone = "555-0100",
FirstName = "Pat",
LastName = "Owner",
Email = "owner@example.com",
Plan = 1
};
var result = await controller.Create(model);
var view = Assert.IsType<ViewResult>(result);
Assert.Equal("Index", view.ViewName);
Assert.False(controller.ModelState.IsValid);
Assert.Contains(controller.ModelState["Email"]!.Errors, e => e.ErrorMessage.Contains("already exists"));
}
[Fact]
public async Task Create_WhenRegistrationIsClosed_ReturnsIndexWithTempDataError()
{
await using var context = CreateContext();
SeedPlanConfig(context, plan: 1);
context.Companies.Add(new Company
{
Id = 1,
CompanyId = 1,
CompanyName = "Existing Company",
CompanyCode = "EXC",
PrimaryContactName = "Owner",
PrimaryContactEmail = "existing@example.com",
IsActive = true
});
await context.SaveChangesAsync();
var platformSettings = new Mock<IPlatformSettingsService>();
platformSettings
.Setup(x => x.GetAsync(PlatformSettingKeys.MaxTenants))
.ReturnsAsync("1");
platformSettings
.Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.MaxTenants)))
.ReturnsAsync((string?)null);
var controller = CreateController(context, platformSettings: platformSettings);
var model = new RegisterCompanyDto
{
CompanyName = "New Co",
CompanyPhone = "555-0100",
FirstName = "Pat",
LastName = "Owner",
Email = "owner@example.com",
Plan = 1
};
var result = await controller.Create(model);
var view = Assert.IsType<ViewResult>(result);
Assert.Equal("Index", view.ViewName);
Assert.Equal("Registration is currently closed. Please contact us for more information.", controller.TempData["Error"]);
Assert.False((bool)controller.ViewBag.RegistrationOpen);
}
[Fact]
public async Task Create_WhenTrialsDisabledAndStripeCheckoutStarts_RedirectsAndPersistsPendingSession()
{
await using var context = CreateContext();
SeedPlanConfig(context, plan: 1);
await context.SaveChangesAsync();
var platformSettings = new Mock<IPlatformSettingsService>();
platformSettings.Setup(x => x.GetAsync(PlatformSettingKeys.TrialsEnabled)).ReturnsAsync("false");
platformSettings.Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.TrialsEnabled))).ReturnsAsync((string?)null);
var stripeService = new Mock<IStripeService>();
stripeService
.Setup(x => x.CreateRegistrationCheckoutSessionAsync(
1, false, "paid@example.com", "Paid Co", It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync("https://checkout.example/session");
var controller = CreateController(
context,
stripeService: stripeService,
platformSettings: platformSettings);
var model = new RegisterCompanyDto
{
CompanyName = "Paid Co",
CompanyPhone = "555-0100",
FirstName = "Pat",
LastName = "Owner",
Email = "paid@example.com",
Plan = 1
};
var result = await controller.Create(model);
var redirect = Assert.IsType<RedirectResult>(result);
Assert.Equal("https://checkout.example/session", redirect.Url);
var pending = await context.PendingRegistrationSessions.SingleAsync();
Assert.Equal("Paid Co", pending.CompanyName);
Assert.Equal("paid@example.com", pending.Email);
Assert.False(pending.IsCompleted);
}
[Fact]
public async Task Create_WhenStripeConfigFails_DoesNotPersistPendingSession()
{
await using var context = CreateContext();
SeedPlanConfig(context, plan: 1);
await context.SaveChangesAsync();
var platformSettings = new Mock<IPlatformSettingsService>();
platformSettings.Setup(x => x.GetAsync(PlatformSettingKeys.TrialsEnabled)).ReturnsAsync("false");
platformSettings.Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.TrialsEnabled))).ReturnsAsync((string?)null);
var stripeService = new Mock<IStripeService>();
stripeService
.Setup(x => x.CreateRegistrationCheckoutSessionAsync(
It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.ThrowsAsync(new InvalidOperationException("Stripe prices are not configured."));
var controller = CreateController(
context,
stripeService: stripeService,
platformSettings: platformSettings);
var model = new RegisterCompanyDto
{
CompanyName = "Paid Co",
CompanyPhone = "555-0100",
FirstName = "Pat",
LastName = "Owner",
Email = "paid@example.com",
Plan = 1
};
var result = await controller.Create(model);
var view = Assert.IsType<ViewResult>(result);
Assert.Equal("Index", view.ViewName);
Assert.Empty(context.PendingRegistrationSessions);
Assert.Contains(controller.ModelState[string.Empty]!.Errors, e => e.ErrorMessage.Contains("Stripe prices are not configured."));
}
[Fact]
public async Task PaymentSuccess_WhenRegistrationClosedAfterPayment_ReleasesPendingSessionWithoutCreatingCompany()
{
await using var context = CreateContext();
SeedPlanConfig(context, plan: 1);
context.Companies.Add(new Company
{
Id = 1,
CompanyId = 1,
CompanyName = "At Capacity",
CompanyCode = "CAP",
PrimaryContactName = "Owner",
PrimaryContactEmail = "capacity@example.com",
IsActive = true
});
context.PendingRegistrationSessions.Add(CreatePendingSession("token-closed", "closed@example.com"));
await context.SaveChangesAsync();
var platformSettings = new Mock<IPlatformSettingsService>();
platformSettings
.Setup(x => x.GetAsync(PlatformSettingKeys.MaxTenants))
.ReturnsAsync("1");
platformSettings
.Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.MaxTenants)))
.ReturnsAsync((string?)null);
var userManager = CreateUserManagerMock();
userManager.Setup(x => x.FindByEmailAsync("closed@example.com")).ReturnsAsync((ApplicationUser?)null);
var stripeService = new Mock<IStripeService>();
stripeService.Setup(x => x.IsRegistrationCheckoutPaidAsync("sess_paid")).ReturnsAsync(true);
var controller = CreateController(
context,
userManager: userManager,
stripeService: stripeService,
platformSettings: platformSettings);
var result = await controller.PaymentSuccess("sess_paid", "token-closed");
var redirect = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", redirect.ActionName);
Assert.Equal("Registration is currently closed. Your payment has been received but no account was created. Please contact support.", controller.TempData["Error"]);
Assert.False((await context.PendingRegistrationSessions.SingleAsync()).IsCompleted);
Assert.Single(context.Companies);
}
[Fact]
public async Task PaymentSuccess_WhenSessionIdMissing_RedirectsToIndex()
{
await using var context = CreateContext();
var controller = CreateController(context);
var result = await controller.PaymentSuccess(null, "token");
var redirect = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", redirect.ActionName);
}
[Fact]
public async Task PaymentSuccess_WhenRegistrationTokenMissing_SetsExpiredError()
{
await using var context = CreateContext();
var controller = CreateController(context);
var result = await controller.PaymentSuccess("sess_123", null);
var redirect = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", redirect.ActionName);
Assert.Equal("Your registration session has expired. Please fill in your details again.", controller.TempData["Error"]);
}
[Fact]
public async Task PaymentSuccess_WhenPendingSessionMissing_SetsNotFoundError()
{
await using var context = CreateContext();
var controller = CreateController(context);
var result = await controller.PaymentSuccess("sess_123", "missing-token");
var redirect = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", redirect.ActionName);
Assert.Equal("Your registration session was not found. Please fill in your details again.", controller.TempData["Error"]);
}
[Fact]
public async Task PaymentSuccess_WhenSessionAlreadyCompletedButUserMissing_ShowsSupportError()
{
await using var context = CreateContext();
context.PendingRegistrationSessions.Add(CreatePendingSession("token-missing-user", "retry@example.com", isCompleted: true));
await context.SaveChangesAsync();
var userManager = CreateUserManagerMock();
userManager.Setup(x => x.FindByEmailAsync("retry@example.com")).ReturnsAsync((ApplicationUser?)null);
var controller = CreateController(context, userManager: userManager);
var result = await controller.PaymentSuccess("sess_done", "token-missing-user");
var redirect = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", redirect.ActionName);
Assert.Contains("couldn't finish signing you in", controller.TempData["Error"]?.ToString());
}
[Fact]
public async Task PaymentCancelled_WhenPendingSessionExists_PrefillsTempDataAndDeletesSession()
{
await using var context = CreateContext();
context.PendingRegistrationSessions.Add(CreatePendingSession("token-cancel", "cancel@example.com"));
await context.SaveChangesAsync();
var controller = CreateController(context);
var result = await controller.PaymentCancelled("token-cancel");
var redirect = Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", redirect.ActionName);
var json = Assert.IsType<string>(controller.TempData["PendingRegistrationJson"]);
var model = JsonSerializer.Deserialize<RegisterCompanyDto>(json);
Assert.NotNull(model);
Assert.Equal("Retry Co", model!.CompanyName);
Assert.Equal("cancel@example.com", model.Email);
Assert.Empty(context.PendingRegistrationSessions);
}
private static RegistrationController CreateController(
ApplicationDbContext context,
Mock<UserManager<ApplicationUser>>? userManager = null,
SignInManager<ApplicationUser>? signInManager = null,
Mock<IStripeService>? stripeService = null)
Mock<IStripeService>? stripeService = null,
Mock<IPlatformSettingsService>? platformSettings = null)
{
var unitOfWork = new UnitOfWork(context);
var userManagerMock = userManager ?? CreateUserManagerMock();
var signInManagerInstance = signInManager ?? CreateSignInManagerMock(userManagerMock.Object).Object;
var platformSettings = new Mock<IPlatformSettingsService>();
platformSettings.Setup(x => x.GetAsync(It.IsAny<string>())).ReturnsAsync((string?)null);
var platformSettingsMock = platformSettings ?? new Mock<IPlatformSettingsService>();
if (platformSettings is null)
{
platformSettingsMock.Setup(x => x.GetAsync(It.IsAny<string>())).ReturnsAsync((string?)null);
}
var controller = new RegistrationController(
unitOfWork,
@@ -116,7 +418,7 @@ public class RegistrationControllerTests
Mock.Of<ISeedDataService>(),
Mock.Of<IAdminNotificationService>(),
Mock.Of<IInAppNotificationService>(),
platformSettings.Object,
platformSettingsMock.Object,
(stripeService ?? new Mock<IStripeService>()).Object,
Mock.Of<IEmailService>(),
Mock.Of<ILogger<RegistrationController>>());
@@ -126,6 +428,11 @@ public class RegistrationControllerTests
{
HttpContext = httpContext
};
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(x => x.Action(It.IsAny<UrlActionContext>()))
.Returns<UrlActionContext>(ctx => $"https://example.test/{ctx.Action}");
controller.Url = urlHelper.Object;
controller.TempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
return controller;
@@ -172,6 +479,19 @@ public class RegistrationControllerTests
return new ApplicationDbContext(options);
}
private static void SeedPlanConfig(ApplicationDbContext context, int plan)
{
context.SubscriptionPlanConfigs.Add(new SubscriptionPlanConfig
{
Id = plan,
CompanyId = 0,
Plan = plan,
DisplayName = $"Plan {plan}",
SortOrder = plan,
IsActive = true
});
}
private static PendingRegistrationSession CreatePendingSession(string token, string email, bool isCompleted = false)
{
return new PendingRegistrationSession