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:
Dev
2026-04-02 12:25:01 +02:00
parent 02955199f6
commit 158aab96b2
3 changed files with 217 additions and 3 deletions

View File

@@ -15,6 +15,7 @@ namespace SharepointToolbox.Infrastructure.Auth;
public class MsalClientFactory
{
private readonly Dictionary<string, IPublicClientApplication> _clients = new();
private readonly Dictionary<string, MsalCacheHelper> _helpers = new();
private readonly SemaphoreSlim _lock = new(1, 1);
/// <summary>Cache directory for MSAL token files.</summary>
@@ -62,8 +63,22 @@ public class MsalClientFactory
helper.RegisterCache(pca.UserTokenCache);
_clients[clientId] = pca;
_helpers[clientId] = helper;
return pca;
}
finally { _lock.Release(); }
}
/// <summary>
/// Returns the MsalCacheHelper for the given clientId.
/// GetOrCreateAsync must be called first — throws InvalidOperationException otherwise.
/// Used by SessionManager to wire PnP's internal token cache to the same persistent cache file.
/// </summary>
public MsalCacheHelper GetCacheHelper(string clientId)
{
if (!_helpers.TryGetValue(clientId, out var helper))
throw new InvalidOperationException(
$"No cache helper found for clientId '{clientId}'. Call GetOrCreateAsync first.");
return helper;
}
}