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