Files
PowderCoatingLogix/tests/PowderCoating.UnitTests/UsageQuotaControllerTests.cs
T
spouliot dbe4170986 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>
2026-04-25 18:27:30 -04:00

330 lines
13 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Web.Controllers;
namespace PowderCoating.UnitTests;
public class UsageQuotaControllerTests
{
[Fact]
public async Task Index_UsesOverridesAndCountsOnlyActiveResources()
{
await using var context = CreateContext();
SeedPlan(context, plan: 1, maxUsers: 10, maxJobs: 5, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10);
SeedCompany(context, companyId: 1, plan: 1, maxUsersOverride: 5);
SeedLookupRows(context, companyId: 1);
context.Users.AddRange(
CreateUser("u1", 1),
CreateUser("u2", 1),
CreateUser("u3", 1),
CreateUser("u4", 1));
context.Customers.AddRange(
new Customer { Id = 1, CompanyId = 1, CompanyName = "Cust 1" },
new Customer { Id = 2, CompanyId = 1, CompanyName = "Cust 2" });
context.Jobs.AddRange(
new Job { Id = 1, CompanyId = 1, CustomerId = 1, Description = "Active", JobNumber = "JOB-1", JobStatusId = 10, JobPriorityId = 1 },
new Job { Id = 2, CompanyId = 1, CustomerId = 1, Description = "Done", JobNumber = "JOB-2", JobStatusId = 11, JobPriorityId = 1 });
context.Quotes.AddRange(
new Quote { Id = 1, CompanyId = 1, QuoteNumber = "Q-1", QuoteStatusId = 10 },
new Quote { Id = 2, CompanyId = 1, QuoteNumber = "Q-2", QuoteStatusId = 11 },
new Quote { Id = 3, CompanyId = 1, QuoteNumber = "Q-3", QuoteStatusId = 12 });
context.CatalogItems.AddRange(
new CatalogItem { Id = 1, CompanyId = 1, Name = "Wheel", CategoryId = 1 },
new CatalogItem { Id = 2, CompanyId = 1, Name = "Frame", CategoryId = 1 });
await context.SaveChangesAsync();
var controller = new UsageQuotaController(context);
var result = await controller.Index(null, null, null, null);
var view = Assert.IsType<ViewResult>(result);
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(view.Model));
Assert.Equal(4, row.Users);
Assert.Equal(5, row.MaxUsers);
Assert.Equal(1, row.ActiveJobs);
Assert.Equal(1, row.ActiveQuotes);
Assert.Equal(2, row.Customers);
Assert.Equal(2, row.CatalogItems);
Assert.True(row.IsNearLimit);
Assert.False(row.IsAtLimit);
Assert.Equal(0, controller.ViewBag.AtLimitCount);
Assert.Equal(1, controller.ViewBag.NearLimitCount);
}
[Fact]
public async Task Index_ForCompedCompany_ReturnsUnlimitedLimitsWithoutFlags()
{
await using var context = CreateContext();
SeedPlan(context, plan: 2, maxUsers: 1, maxJobs: 1, maxCustomers: 1, maxQuotes: 1, maxCatalogItems: 1);
SeedCompany(context, companyId: 2, plan: 2, isComped: true);
SeedLookupRows(context, companyId: 2);
context.Users.AddRange(CreateUser("u1", 2), CreateUser("u2", 2));
context.Customers.Add(new Customer { Id = 10, CompanyId = 2, CompanyName = "Comped Customer" });
context.Jobs.Add(new Job { Id = 10, CompanyId = 2, CustomerId = 10, Description = "Active", JobNumber = "JOB-C", JobStatusId = 20, JobPriorityId = 2 });
context.Quotes.Add(new Quote { Id = 10, CompanyId = 2, QuoteNumber = "Q-C", QuoteStatusId = 20 });
context.CatalogItems.Add(new CatalogItem { Id = 10, CompanyId = 2, Name = "Rack", CategoryId = 1 });
await context.SaveChangesAsync();
var controller = new UsageQuotaController(context);
var result = await controller.Index(null, null, null, null);
var view = Assert.IsType<ViewResult>(result);
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(view.Model));
Assert.True(row.IsComped);
Assert.Equal(-1, row.MaxUsers);
Assert.Equal(-1, row.MaxActiveJobs);
Assert.Equal(-1, row.MaxCustomers);
Assert.Equal(-1, row.MaxActiveQuotes);
Assert.Equal(-1, row.MaxCatalogItems);
Assert.False(row.IsNearLimit);
Assert.False(row.IsAtLimit);
}
[Fact]
public async Task Index_ConcernFilters_SeparateNearAndAtLimitRows()
{
await using var context = CreateContext();
SeedPlan(context, plan: 3, maxUsers: 5, maxJobs: 5, maxCustomers: 5, maxQuotes: 5, maxCatalogItems: 5);
SeedCompany(context, companyId: 3, plan: 3, companyName: "Near Co");
SeedCompany(context, companyId: 4, plan: 3, companyName: "At Co");
SeedCompany(context, companyId: 5, plan: 3, companyName: "Safe Co");
SeedLookupRows(context, 3);
SeedLookupRows(context, 4);
SeedLookupRows(context, 5);
context.Users.AddRange(
CreateUser("n1", 3), CreateUser("n2", 3), CreateUser("n3", 3), CreateUser("n4", 3),
CreateUser("a1", 4), CreateUser("a2", 4), CreateUser("a3", 4), CreateUser("a4", 4), CreateUser("a5", 4),
CreateUser("s1", 5));
await context.SaveChangesAsync();
var controller = new UsageQuotaController(context);
var limitResult = await controller.Index(null, null, null, "limit");
var limitRows = Assert.IsAssignableFrom<List<UsageRow>>(Assert.IsType<ViewResult>(limitResult).Model);
Assert.Equal(2, limitRows.Count);
Assert.Contains(limitRows, r => r.CompanyName == "Near Co" && r.IsNearLimit);
Assert.Contains(limitRows, r => r.CompanyName == "At Co" && r.IsAtLimit);
var atLimitResult = await controller.Index(null, null, null, "atlimit");
var atLimitRows = Assert.IsAssignableFrom<List<UsageRow>>(Assert.IsType<ViewResult>(atLimitResult).Model);
var atLimitRow = Assert.Single(atLimitRows);
Assert.Equal("At Co", atLimitRow.CompanyName);
Assert.True(atLimitRow.IsAtLimit);
}
[Fact]
public async Task Index_AppliesSearchStatusAndPlanFilters()
{
await using var context = CreateContext();
SeedPlan(context, plan: 6, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10, displayName: "Plan Six");
SeedPlan(context, plan: 7, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10, displayName: "Plan Seven");
SeedCompany(context, companyId: 6, plan: 6, companyName: "Acme Powder", status: SubscriptionStatus.Active);
SeedCompany(context, companyId: 7, plan: 6, companyName: "Beta Powder", status: SubscriptionStatus.Expired);
SeedCompany(context, companyId: 8, plan: 7, companyName: "Acme East", status: SubscriptionStatus.Active);
await context.SaveChangesAsync();
var controller = new UsageQuotaController(context);
var result = await controller.Index("Acme", nameof(SubscriptionStatus.Active), "6", null);
var view = Assert.IsType<ViewResult>(result);
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(view.Model));
Assert.Equal("Acme Powder", row.CompanyName);
Assert.Equal(nameof(SubscriptionStatus.Active), controller.ViewBag.StatusFilter);
Assert.Equal("6", controller.ViewBag.PlanFilter);
}
[Fact]
public async Task Index_WhenUsageIsExactlyEightyPercent_MarksRowNearLimit()
{
await using var context = CreateContext();
SeedPlan(context, plan: 8, maxUsers: 5, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10);
SeedCompany(context, companyId: 9, plan: 8, companyName: "Threshold Co");
await context.SaveChangesAsync();
context.Users.AddRange(
CreateUser("t1", 9),
CreateUser("t2", 9),
CreateUser("t3", 9),
CreateUser("t4", 9));
await context.SaveChangesAsync();
var controller = new UsageQuotaController(context);
var result = await controller.Index(null, null, null, null);
var view = Assert.IsType<ViewResult>(result);
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(view.Model));
Assert.Equal(4, row.Users);
Assert.Equal(5, row.MaxUsers);
Assert.True(row.IsNearLimit);
Assert.False(row.IsAtLimit);
}
[Fact]
public async Task Index_WhenFiltersAreInvalid_IgnoresThem()
{
await using var context = CreateContext();
SeedPlan(context, plan: 9, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10);
SeedPlan(context, plan: 10, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10);
SeedCompany(context, companyId: 10, plan: 9, companyName: "Alpha Co", status: SubscriptionStatus.Active);
SeedCompany(context, companyId: 11, plan: 10, companyName: "Beta Co", status: SubscriptionStatus.Expired);
await context.SaveChangesAsync();
var controller = new UsageQuotaController(context);
var result = await controller.Index(null, "NotARealStatus", "not-a-plan", null);
var view = Assert.IsType<ViewResult>(result);
var rows = Assert.IsAssignableFrom<List<UsageRow>>(view.Model);
Assert.Equal(2, rows.Count);
Assert.Equal(2, controller.ViewBag.TotalCount);
Assert.Equal("NotARealStatus", controller.ViewBag.StatusFilter);
Assert.Equal("not-a-plan", controller.ViewBag.PlanFilter);
}
private static ApplicationUser CreateUser(string id, int companyId)
{
return new ApplicationUser
{
Id = id,
CompanyId = companyId,
UserName = $"{id}@example.com",
Email = $"{id}@example.com",
FirstName = "Test",
LastName = "User"
};
}
private static void SeedPlan(
ApplicationDbContext context,
int plan,
int maxUsers,
int maxJobs,
int maxCustomers,
int maxQuotes,
int maxCatalogItems,
string? displayName = null)
{
context.SubscriptionPlanConfigs.Add(new SubscriptionPlanConfig
{
Id = plan,
CompanyId = 0,
Plan = plan,
DisplayName = displayName ?? $"Plan {plan}",
SortOrder = plan,
IsActive = true,
MaxUsers = maxUsers,
MaxActiveJobs = maxJobs,
MaxCustomers = maxCustomers,
MaxQuotes = maxQuotes,
MaxCatalogItems = maxCatalogItems
});
}
private static void SeedCompany(
ApplicationDbContext context,
int companyId,
int plan,
string? companyName = null,
SubscriptionStatus status = SubscriptionStatus.Active,
bool isComped = false,
int? maxUsersOverride = null)
{
context.Companies.Add(new Company
{
Id = companyId,
CompanyId = companyId,
CompanyName = companyName ?? $"Company {companyId}",
PrimaryContactName = "Owner",
PrimaryContactEmail = $"owner{companyId}@example.com",
SubscriptionPlan = plan,
SubscriptionStatus = status,
IsComped = isComped,
MaxUsersOverride = maxUsersOverride,
IsActive = true
});
}
private static void SeedLookupRows(ApplicationDbContext context, int companyId)
{
context.JobPriorityLookups.Add(new JobPriorityLookup
{
Id = companyId,
CompanyId = companyId,
PriorityCode = "NORMAL",
DisplayName = "Normal",
DisplayOrder = 1
});
context.JobStatusLookups.AddRange(
new JobStatusLookup
{
Id = companyId * 10,
CompanyId = companyId,
StatusCode = "ACTIVE",
DisplayName = "Active",
DisplayOrder = 1,
IsTerminalStatus = false
},
new JobStatusLookup
{
Id = companyId * 10 + 1,
CompanyId = companyId,
StatusCode = "DONE",
DisplayName = "Done",
DisplayOrder = 2,
IsTerminalStatus = true
});
context.QuoteStatusLookups.AddRange(
new QuoteStatusLookup
{
Id = companyId * 10,
CompanyId = companyId,
StatusCode = "PENDING",
DisplayName = "Pending",
DisplayOrder = 1
},
new QuoteStatusLookup
{
Id = companyId * 10 + 1,
CompanyId = companyId,
StatusCode = "REJECTED",
DisplayName = "Rejected",
DisplayOrder = 2,
IsRejectedStatus = true
},
new QuoteStatusLookup
{
Id = companyId * 10 + 2,
CompanyId = companyId,
StatusCode = "CONVERTED",
DisplayName = "Converted",
DisplayOrder = 3,
IsConvertedStatus = true
});
}
private static ApplicationDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
}