test(19-01): add unit tests for AppRegistrationService and models

- AppRegistrationResult factory methods (Success/Failure/FallbackRequired)
- TenantProfile.AppId null default and JSON round-trip
- AppRegistrationService implements IAppRegistrationService
- BuildRequiredResourceAccess structure (2 resources, 4+1 scopes, all Scope type)
This commit is contained in:
Dev
2026-04-09 15:14:02 +02:00
parent 93dbb8c5b0
commit 8083cdf7f5

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
[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<TenantProfile>(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<TenantProfile>(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<ISessionManager>();
var loggerMock = new Microsoft.Extensions.Logging.Abstractions.NullLogger<AppRegistrationService>();
var service = new AppRegistrationService(graphFactory, msalFactory, sessionManagerMock.Object, loggerMock);
Assert.IsAssignableFrom<IAppRegistrationService>(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);
}
}