using System.IO; using System.Text.Json; using Moq; using SharepointToolbox.Core.Models; using SharepointToolbox.Services; using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory; using AppMsalClientFactory = SharepointToolbox.Infrastructure.Auth.MsalClientFactory; namespace SharepointToolbox.Tests.Services; /// /// Unit tests for AppRegistrationResult, TenantProfile.AppId, and AppRegistrationService. /// Graph API calls require live Entra connectivity and are marked as Integration tests. /// Pure logic (model behaviour, BuildRequiredResourceAccess structure) is covered here. /// [Trait("Category", "Unit")] public class AppRegistrationServiceTests { // ──────────────────────────────────────────────────────────────────────── // AppRegistrationResult — factory method tests // ──────────────────────────────────────────────────────────────────────── [Fact] public void Success_CarriesAppId() { var result = AppRegistrationResult.Success("appId123"); Assert.True(result.IsSuccess); Assert.False(result.IsFallback); Assert.Equal("appId123", result.AppId); Assert.Null(result.ErrorMessage); } [Fact] public void Failure_CarriesMessage() { var result = AppRegistrationResult.Failure("Something went wrong"); Assert.False(result.IsSuccess); Assert.False(result.IsFallback); Assert.Null(result.AppId); Assert.Equal("Something went wrong", result.ErrorMessage); } [Fact] public void FallbackRequired_SetsFallback() { var result = AppRegistrationResult.FallbackRequired(); Assert.False(result.IsSuccess); Assert.True(result.IsFallback); Assert.Null(result.AppId); Assert.Null(result.ErrorMessage); } // ──────────────────────────────────────────────────────────────────────── // TenantProfile.AppId — nullable field tests // ──────────────────────────────────────────────────────────────────────── [Fact] public void AppId_DefaultsToNull() { var profile = new TenantProfile(); Assert.Null(profile.AppId); } [Fact] public void AppId_RoundTrips_ViaJson() { var profile = new TenantProfile { Name = "Test Tenant", TenantUrl = "https://example.sharepoint.com", ClientId = "client-id-abc", AppId = "registered-app-id-xyz" }; var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var json = JsonSerializer.Serialize(profile, options); var loaded = JsonSerializer.Deserialize(json, options); Assert.NotNull(loaded); Assert.Equal("registered-app-id-xyz", loaded!.AppId); Assert.Equal("Test Tenant", loaded.Name); } [Fact] public void AppId_Null_RoundTrips_ViaJson() { var profile = new TenantProfile { Name = "Test Tenant", TenantUrl = "https://example.sharepoint.com", ClientId = "client-id-abc", AppId = null }; var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var json = JsonSerializer.Serialize(profile, options); var loaded = JsonSerializer.Deserialize(json, options); Assert.NotNull(loaded); Assert.Null(loaded!.AppId); } // ──────────────────────────────────────────────────────────────────────── // AppRegistrationService — constructor / dependency wiring // ──────────────────────────────────────────────────────────────────────── [Fact] public void AppRegistrationService_ImplementsInterface() { // Verify that the concrete class satisfies the interface contract. // We instantiate with a real MsalClientFactory (no-IO path) and mocked session manager / logger. var msalFactory = new AppMsalClientFactory(Path.GetTempPath()); var graphFactory = new AppGraphClientFactory(msalFactory); var sessionManagerMock = new Mock(); var loggerMock = new Microsoft.Extensions.Logging.Abstractions.NullLogger(); var service = new AppRegistrationService(graphFactory, msalFactory, sessionManagerMock.Object, loggerMock); Assert.IsAssignableFrom(service); } // ──────────────────────────────────────────────────────────────────────── // BuildRequiredResourceAccess — structure verification (no live calls) // ──────────────────────────────────────────────────────────────────────── [Fact] public void BuildRequiredResourceAccess_ContainsTwoResources() { var result = AppRegistrationService.BuildRequiredResourceAccess(); Assert.Equal(2, result.Count); } [Fact] public void BuildRequiredResourceAccess_GraphResource_HasFourScopes() { const string graphAppId = "00000003-0000-0000-c000-000000000000"; var result = AppRegistrationService.BuildRequiredResourceAccess(); var graphEntry = result.Single(r => r.ResourceAppId == graphAppId); Assert.NotNull(graphEntry.ResourceAccess); Assert.Equal(4, graphEntry.ResourceAccess!.Count); } [Fact] public void BuildRequiredResourceAccess_SharePointResource_HasOneScope() { const string spoAppId = "00000003-0000-0ff1-ce00-000000000000"; var result = AppRegistrationService.BuildRequiredResourceAccess(); var spoEntry = result.Single(r => r.ResourceAppId == spoAppId); Assert.NotNull(spoEntry.ResourceAccess); Assert.Single(spoEntry.ResourceAccess!); } [Fact] public void BuildRequiredResourceAccess_AllScopes_HaveScopeType() { var result = AppRegistrationService.BuildRequiredResourceAccess(); var allAccess = result.SelectMany(r => r.ResourceAccess!); Assert.All(allAccess, ra => Assert.Equal("Scope", ra.Type)); } [Fact] public void BuildRequiredResourceAccess_GraphResource_ContainsUserReadScope() { const string graphAppId = "00000003-0000-0000-c000-000000000000"; var userReadGuid = Guid.Parse("e1fe6dd8-ba31-4d61-89e7-88639da4683d"); // User.Read var result = AppRegistrationService.BuildRequiredResourceAccess(); var graphEntry = result.Single(r => r.ResourceAppId == graphAppId); Assert.Contains(graphEntry.ResourceAccess!, ra => ra.Id == userReadGuid); } }