using AutopilotMonitor.Functions.Functions.Infrastructure;
using AutopilotMonitor.Functions.Security;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Shared.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Moq;
using System.Net;
using System.Security.Claims;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.DependencyInjection;
namespace AutopilotMonitor.Functions.Tests;
///
/// Tests for the side-effect methods extracted from AuthFunction.GetCurrentUser().
/// Uses Moq to verify service interactions (domain persistence, auto-re-enable, auto-admin, metrics).
///
public class AuthFunctionSideEffectTests
{
private const string TenantId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
private const string Upn = "user@contoso.com";
private const string DisplayName = "Test User";
private const string ObjectId = "oid-12345";
private readonly Mock _tenantConfigMock;
private readonly Mock _tenantAdminsMock;
private readonly Mock _telegramMock;
private readonly Mock _globalNotificationMock;
private readonly Mock _metricsRepoMock;
private readonly Mock _autoApproveEnqueuerMock;
private readonly FakeSignalRNotificationService _signalR;
private readonly AuthFunction _sut;
public AuthFunctionSideEffectTests()
{
// Shared interface mocks for constructor injection
var adminRepo = Mock.Of();
var configRepo = Mock.Of();
var notificationRepo = Mock.Of();
var cache = Mock.Of();
_tenantConfigMock = new Mock(
configRepo, Mock.Of>(), cache)
{ CallBase = false };
var bindings = new StubAdminIdentityBindingService(bound: true);
var globalAdminMock = new Mock(
adminRepo, bindings, cache, Mock.Of>())
{ CallBase = false };
var delegatedAdminMock = new Mock(
adminRepo,
bindings,
new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Pro),
cache, Mock.Of>())
{ CallBase = false };
delegatedAdminMock.Setup(x => x.GetScopeAsync(It.IsAny()))
.ReturnsAsync(DelegatedScope.Empty);
_tenantAdminsMock = new Mock(
adminRepo, cache, Mock.Of>())
{ CallBase = false };
var previewMock = new Mock(
configRepo, cache, Mock.Of>(), _tenantConfigMock.Object)
{ CallBase = false };
_telegramMock = new Mock(
new HttpClient(), configRepo, Mock.Of>())
{ CallBase = false };
_globalNotificationMock = new Mock(
notificationRepo, new FakeSignalRNotificationService(), Mock.Of>())
{ CallBase = false };
var adminConfigService = new Mock(
configRepo, Mock.Of>(), cache)
{ CallBase = false };
var mcpUserMock = new Mock(
adminRepo, new StubAdminIdentityBindingService(bound: true), cache, Mock.Of>(),
globalAdminMock.Object, delegatedAdminMock.Object, adminConfigService.Object)
{ CallBase = false };
_metricsRepoMock = new Mock();
_autoApproveEnqueuerMock = new Mock();
_signalR = new FakeSignalRNotificationService();
_sut = new AuthFunction(
Mock.Of>(),
globalAdminMock.Object,
delegatedAdminMock.Object,
_tenantConfigMock.Object,
_tenantAdminsMock.Object,
_metricsRepoMock.Object,
previewMock.Object,
_telegramMock.Object,
_globalNotificationMock.Object,
mcpUserMock.Object,
_autoApproveEnqueuerMock.Object,
new AutopilotMonitor.Functions.Security.EntraAppRegistry(
new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(),
Mock.Of>()),
new Mock(
_metricsRepoMock.Object, _tenantConfigMock.Object, Mock.Of>()) { CallBase = false }.Object,
_signalR);
// Default: all fire-and-forget calls succeed
_tenantConfigMock
.Setup(x => x.SaveConfigurationAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
_telegramMock
.Setup(x => x.SendNewTenantSignupAsync(It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
_globalNotificationMock
.Setup(x => x.CreateNotificationAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
_metricsRepoMock
.Setup(x => x.RecordUserLoginAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
}
private static TenantConfiguration DefaultConfig() => TenantConfiguration.CreateDefault(TenantId);
// -------------------------------------------------------------------------
// HandleNewTenantDomainAsync
// -------------------------------------------------------------------------
// ── RemoveGlobalAdmin cuts live SignalR streams (join-time-only group authz) ──
[Fact]
public async Task RemoveGlobalAdmin_DisconnectsTheRemovedAdminsSignalRConnections()
{
var (req, ctx) = BuildAuthenticatedRequest(callerUpn: "operator@contoso.com");
var response = await _sut.RemoveGlobalAdmin(req, "Removed@Contoso.com", ctx);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(new[] { "removed@contoso.com" }, _signalR.DisconnectedUsers);
}
[Fact]
public async Task RemoveGlobalAdmin_SelfRemovalIsRejectedWithoutDisconnect()
{
var (req, ctx) = BuildAuthenticatedRequest(callerUpn: "operator@contoso.com");
var response = await _sut.RemoveGlobalAdmin(req, "operator@contoso.com", ctx);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Empty(_signalR.DisconnectedUsers);
}
private static (HttpRequestData Req, FunctionContext Ctx) BuildAuthenticatedRequest(string callerUpn)
{
var services = new ServiceCollection();
services.AddOptions();
services.Configure(o => o.Serializer =
new Azure.Core.Serialization.JsonObjectSerializer(
new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web)));
var provider = services.BuildServiceProvider();
var principal = new ClaimsPrincipal(new ClaimsIdentity(
new[] { new Claim("preferred_username", callerUpn) }, "test"));
var context = new Mock();
context.SetupGet(c => c.Items).Returns(
new Dictionary