feat(01-04): SessionManager singleton holding all ClientContext instances
- 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)
This commit is contained in:
@@ -1,7 +1,103 @@
|
||||
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;
|
||||
|
||||
public class SessionManagerTests
|
||||
[Trait("Category", "Unit")]
|
||||
public class SessionManagerTests : IDisposable
|
||||
{
|
||||
[Fact(Skip = "Wave 0 stub — implemented in plan 01-04")]
|
||||
public void GetContext_Returns_ClientContext_For_ConnectedTenant() { }
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user