- SessionManager owns all ClientContexts; callers must not store references - IsAuthenticated(tenantUrl) returns false before auth, true after GetOrCreateContextAsync - ClearSessionAsync disposes ClientContext and removes state (idempotent for unknown tenants) - GetOrCreateContextAsync validates null/empty TenantUrl and ClientId (ArgumentException) - MsalClientFactory.GetCacheHelper() added — exposes helper for PnP tokenCacheCallback wiring - 8 unit tests pass, 1 interactive-login test skipped (integration-only)
104 lines
4.0 KiB
C#
104 lines
4.0 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using SharepointToolbox.Core.Models;
|
|
using SharepointToolbox.Infrastructure.Auth;
|
|
using SharepointToolbox.Services;
|
|
using Xunit;
|
|
|
|
namespace SharepointToolbox.Tests.Auth;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public class SessionManagerTests : IDisposable
|
|
{
|
|
private readonly string _tempCacheDir;
|
|
private readonly MsalClientFactory _factory;
|
|
private readonly SessionManager _sessionManager;
|
|
|
|
public SessionManagerTests()
|
|
{
|
|
_tempCacheDir = Path.Combine(Path.GetTempPath(), "SessionManagerTests_" + Guid.NewGuid());
|
|
Directory.CreateDirectory(_tempCacheDir);
|
|
_factory = new MsalClientFactory(_tempCacheDir);
|
|
_sessionManager = new SessionManager(_factory);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_tempCacheDir, recursive: true); } catch { /* best effort */ }
|
|
}
|
|
|
|
// ── IsAuthenticated ──────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void IsAuthenticated_BeforeAnyAuth_ReturnsFalse()
|
|
{
|
|
Assert.False(_sessionManager.IsAuthenticated("https://contoso.sharepoint.com"));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsAuthenticated_NormalizesTrailingSlash()
|
|
{
|
|
Assert.False(_sessionManager.IsAuthenticated("https://contoso.sharepoint.com/"));
|
|
Assert.False(_sessionManager.IsAuthenticated("https://contoso.sharepoint.com"));
|
|
}
|
|
|
|
// ── ClearSessionAsync ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task ClearSessionAsync_UnknownTenantUrl_DoesNotThrow()
|
|
{
|
|
// Must be idempotent — no exception for tenants that were never authenticated
|
|
await _sessionManager.ClearSessionAsync("https://unknown.sharepoint.com");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClearSessionAsync_MultipleCalls_DoNotThrow()
|
|
{
|
|
await _sessionManager.ClearSessionAsync("https://contoso.sharepoint.com");
|
|
await _sessionManager.ClearSessionAsync("https://contoso.sharepoint.com");
|
|
}
|
|
|
|
// ── Argument validation ──────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetOrCreateContextAsync_NullTenantUrl_ThrowsArgumentException()
|
|
{
|
|
var profile = new TenantProfile { TenantUrl = null!, ClientId = "clientId", Name = "Test" };
|
|
await Assert.ThrowsAnyAsync<ArgumentException>(
|
|
() => _sessionManager.GetOrCreateContextAsync(profile));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrCreateContextAsync_EmptyTenantUrl_ThrowsArgumentException()
|
|
{
|
|
var profile = new TenantProfile { TenantUrl = "", ClientId = "clientId", Name = "Test" };
|
|
await Assert.ThrowsAnyAsync<ArgumentException>(
|
|
() => _sessionManager.GetOrCreateContextAsync(profile));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrCreateContextAsync_NullClientId_ThrowsArgumentException()
|
|
{
|
|
var profile = new TenantProfile { TenantUrl = "https://contoso.sharepoint.com", ClientId = null!, Name = "Test" };
|
|
await Assert.ThrowsAnyAsync<ArgumentException>(
|
|
() => _sessionManager.GetOrCreateContextAsync(profile));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrCreateContextAsync_EmptyClientId_ThrowsArgumentException()
|
|
{
|
|
var profile = new TenantProfile { TenantUrl = "https://contoso.sharepoint.com", ClientId = "", Name = "Test" };
|
|
await Assert.ThrowsAnyAsync<ArgumentException>(
|
|
() => _sessionManager.GetOrCreateContextAsync(profile));
|
|
}
|
|
|
|
// ── Interactive login test (skipped — requires MSAL interactive flow) ────
|
|
|
|
[Fact(Skip = "Requires interactive MSAL — integration test only")]
|
|
public Task GetOrCreateContextAsync_CreatesContext()
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|