using GB5Shared.Auth.Jwt;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using PartnerBLL.Auth;
using PartnerBLL.Options;
using PartnerDAL.CustomCode.PartnerApiKey;
using PartnerDAL.DTO.PartnerApiKey;
using Xunit;
namespace PartnerTests;
///
/// Covers PartnerBLL.Auth.PartnerTokenBLL — the new Partner M2M client-credentials token
/// issuance flow (Thread 7 of the plan tracker). A partner backend exchanges its existing
/// TPARTNERAPIKEY secret here once for a short-lived JWT via the shared GB5Shared.Auth.Jwt
/// issuer, instead of an unauthenticated PartnerProductId query parameter.
///
public class PartnerTokenBLLTests
{
private const string TestIssuer = "GB5-Partner-Test";
private const string TestAudience = "GB5-Entitlement-Partner-M2M-Test";
private static (PartnerTokenBLL Svc, Mock ApiKeyDal, Mock Issuer)
BuildService(int accessMinutes = 15)
{
var apiKeyDal = new Mock();
var issuer = new Mock();
issuer.Setup(i => i.IssueAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new JwtIssuedAccessToken { AccessToken = "signed-m2m-token", ExpiresOn = DateTime.UtcNow.AddMinutes(15) });
var options = Options.Create(new PartnerM2MJwtOptions
{
Issuer = TestIssuer,
Audience = TestAudience,
AccessTokenMinutes = accessMinutes
});
var svc = new PartnerTokenBLL(apiKeyDal.Object, issuer.Object, options, NullLogger.Instance);
return (svc, apiKeyDal, issuer);
}
[Fact]
public async Task Test_IssueTokenAsync_ValidApiKey_ReturnsSuccessWithToken()
{
var (svc, apiKeyDal, issuer) = BuildService();
apiKeyDal.Setup(d => d.GetApiKeyByHashFromSystemDbAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PartnerApiKeyListDTO
{
ApiKeyId = 5,
PartnerId = 7,
PartnerProductId = 42,
Scopes = "partner,lic",
Status = 1
});
apiKeyDal.Setup(d => d.UpdateApiKeyLastUsedInSystemDbAsync(5, It.IsAny()))
.Returns(Task.CompletedTask);
var result = await svc.IssueTokenAsync("some-valid-secret", CancellationToken.None);
Assert.Equal(PartnerTokenResultStatus.Success, result.Status);
Assert.Equal("signed-m2m-token", result.AccessToken);
Assert.NotNull(result.ExpiresOn);
issuer.Verify(i => i.IssueAsync(
It.Is(c => ((PartnerM2MAccessTokenClaims)c).PartnerId == 7
&& ((PartnerM2MAccessTokenClaims)c).PartnerProductId == 42
&& ((PartnerM2MAccessTokenClaims)c).ApiKeyId == 5
&& ((PartnerM2MAccessTokenClaims)c).Scopes == "partner,lic"),
It.IsAny(), TestIssuer, TestAudience, 15, It.IsAny()),
Times.Once);
}
[Fact]
public async Task Test_IssueTokenAsync_UnknownApiKey_ReturnsInvalidApiKey_NoTokenIssued()
{
var (svc, apiKeyDal, issuer) = BuildService();
apiKeyDal.Setup(d => d.GetApiKeyByHashFromSystemDbAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync((PartnerApiKeyListDTO?)null);
var result = await svc.IssueTokenAsync("wrong-secret", CancellationToken.None);
Assert.Equal(PartnerTokenResultStatus.InvalidApiKey, result.Status);
Assert.Null(result.AccessToken);
issuer.Verify(i => i.IssueAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public async Task Test_IssueTokenAsync_EmptyOrWhitespaceApiKey_ReturnsInvalidApiKey_NeverQueriesDb(string blank)
{
var (svc, apiKeyDal, _) = BuildService();
var result = await svc.IssueTokenAsync(blank, CancellationToken.None);
Assert.Equal(PartnerTokenResultStatus.InvalidApiKey, result.Status);
apiKeyDal.Verify(d => d.GetApiKeyByHashFromSystemDbAsync(It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_IssueTokenAsync_ApiKeyRecordWithNullPartnerId_IsTreatedAsInvalid()
{
// Defensive case: the join in GET_API_KEY_BY_HASH should always populate PartnerId for an
// active key (TPARTNERPRODUCT is an INNER JOIN), but if it's ever null, this must not
// silently issue a token with a garbage PartnerId of 0.
var (svc, apiKeyDal, issuer) = BuildService();
apiKeyDal.Setup(d => d.GetApiKeyByHashFromSystemDbAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new PartnerApiKeyListDTO { ApiKeyId = 5, PartnerId = null, PartnerProductId = 42, Status = 1 });
var result = await svc.IssueTokenAsync("some-secret", CancellationToken.None);
Assert.Equal(PartnerTokenResultStatus.InvalidApiKey, result.Status);
issuer.Verify(i => i.IssueAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
}