diff --git a/SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs b/SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs new file mode 100644 index 0000000..d88fb7c --- /dev/null +++ b/SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs @@ -0,0 +1,178 @@ +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); + } +}