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:
178
SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs
Normal file
178
SharepointToolbox.Tests/Services/AppRegistrationServiceTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user