using Dapr.Client; using EntitlementBLL.Common; using EntitlementBLL.Exceptions; using EntitlementBLL.Implementations; using EntitlementBLL.Options; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.User; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace EntitlementTests; /// /// Covers Thread 1 §2.2 (narrowed) — MCLIENT/MUSER creation. ClientCode-uniqueness rejection; /// happy path (MCLIENT inserted, IClientUserProvisioner.ProvisionAsync called with TenantId scoped /// to the new ClientId via the LoginDTO, TemporaryPassword surfaced from the provisioning result); /// the DefaultAdminRoleCode-not-configured guard and the RoleCode-not-found-in-DB guard (§31.12 — /// RoleId is now resolved by RoleCode via IClientProvisioningDAL.GetRoleIdByCodeAsync, not read /// directly off options as a hardcoded numeric ID). /// public class ClientProvisioningBLLTests { private static (ClientProvisioningBLL Svc, Mock Dal, Mock UserBLL) BuildService(string? defaultAdminRoleCode = "CLADMIN", int? resolvedRoleId = 42) { var dal = new Mock(); dal.Setup(d => d.InsertClientAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); dal.Setup(d => d.GetRoleIdByCodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(resolvedRoleId); var userBLL = new Mock(); userBLL .Setup(u => u.ProvisionAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new ProvisionClientUserResult { UserId = 501, UserGeneratedPassword = "Temp#Pass123" }); var queryExecutor = new Mock(); queryExecutor .Setup(q => q.QueryAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { 1001 }); var dbConfig = new Mock>(); dbConfig.Setup(o => o.Value).Returns(new Gb5SystemDTO { DataBaseType = 0 }); var autoNumber = new AutoNumber(queryExecutor.Object, dbConfig.Object); var loginFactory = new Mock(); loginFactory.Setup(f => f.Create(It.IsAny(), It.IsAny())) .Returns((int clientId, int userId) => new LoginDTO { ClientId = clientId, UserId = userId }); var eventLog = new EventLogPublish(new DaprClientBuilder().Build(), NullLogger.Instance); var options = new Mock>(); options.Setup(o => o.Value).Returns(new ClientProvisioningOptions { DefaultAdminRoleCode = defaultAdminRoleCode! }); var svc = new ClientProvisioningBLL( dal.Object, userBLL.Object, autoNumber, loginFactory.Object, eventLog, options.Object); return (svc, dal, userBLL); } private static CreateClientRequestDTO ValidRequest() => new() { ClientCode = "ACME", ClientName = "Acme Corp", AdminUserCode = "acme_admin", AdminUserName = "Acme Admin", AdminEmail = "admin@acme.test", AdminMobile = "9999999999", }; [Fact] public async Task Test_CreateClientAsync_ClientCodeAlreadyExists_ThrowsAndDoesNotInsert() { var (svc, dal, userBLL) = BuildService(); dal.Setup(d => d.ClientCodeExistsAsync("ACME", It.IsAny(), It.IsAny())) .ReturnsAsync(true); await Assert.ThrowsAsync( () => svc.CreateClientAsync(ValidRequest(), CancellationToken.None)); dal.Verify(d => d.InsertClientAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); userBLL.Verify(u => u.ProvisionAsync(It.IsAny(), It.IsAny()), Times.Never); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public async Task Test_CreateClientAsync_DefaultAdminRoleCodeNotConfigured_Throws(string? roleCode) { var (svc, dal, userBLL) = BuildService(defaultAdminRoleCode: roleCode); dal.Setup(d => d.ClientCodeExistsAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(false); await Assert.ThrowsAsync( () => svc.CreateClientAsync(ValidRequest(), CancellationToken.None)); dal.Verify(d => d.GetRoleIdByCodeAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); userBLL.Verify(u => u.ProvisionAsync(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_CreateClientAsync_RoleCodeNotFoundInDb_Throws() { var (svc, dal, userBLL) = BuildService(defaultAdminRoleCode: "CLADMIN", resolvedRoleId: null); dal.Setup(d => d.ClientCodeExistsAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(false); await Assert.ThrowsAsync( () => svc.CreateClientAsync(ValidRequest(), CancellationToken.None)); dal.Verify(d => d.InsertClientAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); userBLL.Verify(u => u.ProvisionAsync(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Test_CreateClientAsync_HappyPath_InsertsClientAndCreatesAdminUser() { var (svc, dal, userBLL) = BuildService(defaultAdminRoleCode: "CLADMIN", resolvedRoleId: 42); dal.Setup(d => d.ClientCodeExistsAsync("ACME", It.IsAny(), It.IsAny())) .ReturnsAsync(false); var result = await svc.CreateClientAsync(ValidRequest(), CancellationToken.None); dal.Verify(d => d.InsertClientAsync( It.Is(c => c.ClientCode == "ACME" && c.ClientName == "Acme Corp"), It.IsAny(), It.IsAny()), Times.Once); userBLL.Verify(u => u.ProvisionAsync( It.Is(d => d.UserCode == "acme_admin" && d.RoleId == 42), It.Is(l => l.ClientId == result.ClientId)), Times.Once); Assert.Equal(501, result.UserId); Assert.Equal("Temp#Pass123", result.TemporaryPassword); } [Fact] public async Task Test_CreateClientAsync_DeploymentTypeDefaultsToSaaS_WhenNotSupplied() { var (svc, dal, _) = BuildService(defaultAdminRoleCode: "CLADMIN", resolvedRoleId: 42); dal.Setup(d => d.ClientCodeExistsAsync("ACME", It.IsAny(), It.IsAny())) .ReturnsAsync(false); await svc.CreateClientAsync(ValidRequest(), CancellationToken.None); dal.Verify(d => d.InsertClientAsync( It.Is(c => c.DeploymentType == 0), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Test_CreateClientAsync_DeploymentTypeThreadedThrough_WhenCallerSuppliesOnPrem() { // Tracker §52 — the BYOC/On-Prem gating check itself lives in OnboardClient.cs (SL layer, // needs the caller's real MenuRights); this BLL just needs to faithfully persist whatever // value the caller (already authorized) passed in, not silently coerce it back to SaaS. var (svc, dal, _) = BuildService(defaultAdminRoleCode: "CLADMIN", resolvedRoleId: 42); dal.Setup(d => d.ClientCodeExistsAsync("ACME", It.IsAny(), It.IsAny())) .ReturnsAsync(false); var req = ValidRequest(); req.DeploymentType = 1; // OnPrem await svc.CreateClientAsync(req, CancellationToken.None); dal.Verify(d => d.InsertClientAsync( It.Is(c => c.DeploymentType == 1), It.IsAny(), It.IsAny()), Times.Once); } [Theory] [InlineData("", "Acme Corp", "admin", "Admin", "a@b.com", "123")] [InlineData("ACME", "", "admin", "Admin", "a@b.com", "123")] [InlineData("ACME", "Acme Corp", "", "Admin", "a@b.com", "123")] [InlineData("ACME", "Acme Corp", "admin", "", "a@b.com", "123")] [InlineData("ACME", "Acme Corp", "admin", "Admin", "", "123")] [InlineData("ACME", "Acme Corp", "admin", "Admin", "a@b.com", "")] public async Task Test_CreateClientAsync_MissingRequiredField_Throws( string clientCode, string clientName, string adminUserCode, string adminUserName, string adminEmail, string adminMobile) { var (svc, _, _) = BuildService(); var req = new CreateClientRequestDTO { ClientCode = clientCode, ClientName = clientName, AdminUserCode = adminUserCode, AdminUserName = adminUserName, AdminEmail = adminEmail, AdminMobile = adminMobile, }; await Assert.ThrowsAsync(() => svc.CreateClientAsync(req, CancellationToken.None)); } }