Skip to content

chore: migrate Auth0.ManagementApi v7→v8 and bump NuGet packages#266

Merged
mpaulosky merged 7 commits into
devfrom
update/nuget-packages
May 1, 2026
Merged

chore: migrate Auth0.ManagementApi v7→v8 and bump NuGet packages#266
mpaulosky merged 7 commits into
devfrom
update/nuget-packages

Conversation

@mpaulosky
Copy link
Copy Markdown
Owner

Summary

Resolves the breaking changes introduced by Dependabot PR #264 (Auth0.ManagementApi 7.46.0 → 8.1.0 major bump) and bumps remaining packages.

Changes

Directory.Packages.props — version bumps applied (mirrors Dependabot PR #264):

  • Auth0.ManagementApi: 7.46.0 → 8.2.0

src/Web/Features/Admin/Users/UserManagementService.cs

  • Removed manual token-fetch machinery (IHttpClientFactory, TokenCacheService, token endpoint calls)
  • Inject IManagementApiClient directly — v8 handles token caching internally
  • All five public methods updated to v8 call patterns

src/Web/Features/Admin/Users/UserManagementExtensions.cs

  • Replaced AddHttpClient + TokenCacheService registration with AddSingleton<IManagementApiClient> using ManagementClient(ManagementClientOptions { TokenProvider = new ClientCredentialsTokenProvider(...) })

tests/Web.Tests/Services/UserManagementServiceTests.cs

  • Constructor updated to accept IManagementApiClient? instead of IHttpClientFactory?
  • Token-caching tests removed (v8 handles internally)
  • FakeHttpMessageHandler deleted

tests/Web.Tests/Features/Admin/Users/UserManagementServiceCacheTests.cs

  • DefaultOptions/IHttpClientFactory/TokenOnlyHttpClientFactory/FakeHttpMessageHandler all removed
  • CreateSut accepts IManagementApiClient?
  • Cache-miss tests now assert ExternalService error code

Test results (local)

Suite Result
Architecture.Tests ✅ 63/63
Domain.Tests ✅ 419/419
Web.Tests.Bunit ✅ 934/934
Persistence.MongoDb.Tests ✅ 77/77
Web.Tests ✅ 498/498
Persistence.AzureStorage.Tests ✅ 33/33

⚠️ This task was flagged as "needs review" — please have a squad member review before merging.

mpaulosky and others added 2 commits April 30, 2026 11:26
Working as Sam (Backend Developer)

Replaces manual M2M token fetch + IHttpClientFactory with a single
IManagementApiClient singleton registered via ClientCredentialsTokenProvider.

- UserManagementService: new fields/ctor, v8 API calls (Pager<T>,
  UserResponseSchema, GetUserResponseContent, Users.Roles sub-client),
  two MapUser overloads, ParseLastLogin helper; removed GetManagementClientAsync,
  GetOrFetchTokenAsync, TokenResponse
- UserManagementExtensions: add IManagementApiClient singleton registration
- UserManagementServiceTests: remove DefaultOptions/IHttpClientFactory/
  FakeHttpMessageHandler; CreateSut accepts IManagementApiClient?
- UserManagementServiceCacheTests: same constructor cleanup; remove
  TokenOnlyHttpClientFactory, DefaultOptions, FakeHttpMessageHandler;
  DidNotReceive().CreateClient assertions dropped

Closes #264

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 30, 2026 21:09
@github-actions github-actions Bot added the squad Squad triage inbox — Lead will assign to a member label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

🏗️ PR Added to Squad Triage Queue

This PR has been labeled with squad and added to the triage queue.

Next steps:

  • The squad Lead will review and assign to an appropriate team member
  • A squad:member label will be added after triage

If you know which squad member should handle this, you can add the appropriate squad:member label yourself.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

Test Results Summary

2 364 tests  +2 364   2 364 ✅ +2 364   1m 19s ⏱️ + 1m 19s
   10 suites +   10       0 💤 ±    0 
   10 files   +   10       0 ❌ ±    0 

Results for commit d3e9a43. ± Comparison against base commit 0efb404.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the admin User Management integration to Auth0.ManagementApi v8 (removing the custom token-fetching/caching path) and bumps centralized NuGet package versions to match the Dependabot-driven updates.

Changes:

  • Update UserManagementService to use injected IManagementApiClient and v8 request/response models.
  • Register IManagementApiClient in DI using ManagementClientOptions + ClientCredentialsTokenProvider.
  • Refactor unit tests to remove HttpClient/token-caching fixtures and adapt to the new injection model; bump package versions in Directory.Packages.props.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Directory.Packages.props Central package version bumps including Auth0.ManagementApi 8.2.0.
src/Web/Features/Admin/Users/UserManagementService.cs Replace manual token plumbing with injected IManagementApiClient; update v8 API calls and mapping.
src/Web/Features/Admin/Users/UserManagementExtensions.cs DI registration switched to singleton IManagementApiClient using client-credentials token provider.
tests/Web.Tests/Services/UserManagementServiceTests.cs Update SUT construction to inject IManagementApiClient; remove token-caching tests.
tests/Web.Tests/Features/Admin/Users/UserManagementServiceCacheTests.cs Update cache tests to inject IManagementApiClient; remove fake HTTP/token handler infrastructure.

TokenProvider = new ClientCredentialsTokenProvider(
opts.Domain,
opts.ClientId,
opts.ClientSecret)
Comment on lines 84 to 96
public async Task AssignRolesAsync_EmptyRolesList_ReturnsImmediateSuccess()
{
// Arrange — no HttpClientFactory call expected because roles list is empty
var httpClientFactory = Substitute.For<IHttpClientFactory>();
var sut = CreateSut(httpClientFactory: httpClientFactory);
// Arrange — no Management API call expected because roles list is empty
var managementClient = Substitute.For<IManagementApiClient>();
var sut = CreateSut(managementApiClient: managementClient);

// Act
var result = await sut.AssignRolesAsync("auth0|user1", [], CancellationToken.None);

// Assert
result.Success.Should().BeTrue();
result.Value.Should().BeTrue();
httpClientFactory.DidNotReceive().CreateClient(Arg.Any<string>());
}
Comment on lines 26 to 34
/// <summary>
/// Sprint 2 — Unit tests for <see cref="UserManagementService" /> IDistributedCache behaviour.
///
/// These tests use a real <see cref="MemoryDistributedCache" /> so cache read/write round-trips
/// are exercised without mocking serialization internals. The Auth0 Management API layer is
/// replaced by a <see cref="FakeHttpMessageHandler" /> that returns precanned JSON for the
/// M2M token endpoint. Management API calls that would contact Auth0 are expected to fail with
/// <see cref="ResultErrorCode.ExternalService" /> — the tests assert cache behaviour, not the
/// success path of the Management API itself.
/// replaced by an NSubstitute <see cref="IManagementApiClient" /> stub. Cache-miss tests assert
/// <see cref="ResultErrorCode.ExternalService" /> (NSubstitute default returns a null-valued struct
/// which causes NullReferenceException, caught as ExternalService by the service).
/// </summary>
Comment on lines 84 to 108
[Fact]
public async Task ListUsersAsync_SecondCall_HitsCacheAndSkipsAuth0()
{
// Arrange — pre-populate the distributed cache with a serialised user list using
// version=0 (the default when no version entry exists).
var httpClientFactory = Substitute.For<IHttpClientFactory>();
var (sut, distributedCache) = CreateSut(httpClientFactory: httpClientFactory);
var managementClient = Substitute.For<IManagementApiClient>();
var (sut, distributedCache) = CreateSut(managementApiClient: managementClient);
var expectedUsers = new List<AdminUserSummary>
{
new() { UserId = "auth0|u1", Email = "a@test.com", Name = "Alpha", Roles = ["Admin"] },
new() { UserId = "auth0|u2", Email = "b@test.com", Name = "Beta", Roles = ["User"] }
};

// Version 0 is the default; key format: auth0_users_page_{version}_{page}_{perPage}
const string cacheKey = "auth0_users_page_0_1_10";
await PrePopulateCacheAsync(distributedCache, cacheKey, expectedUsers);

// Act
var result = await sut.ListUsersAsync(1, 10, CancellationToken.None);

// Assert
result.Success.Should().BeTrue();
result.Value.Should().HaveCount(2);
result.Value![0].UserId.Should().Be("auth0|u1");
httpClientFactory.DidNotReceive().CreateClient(Arg.Any<string>());
}
Comment on lines +109 to +110
var rolesPager = await _managementClient.Users.Roles
.ListAsync(u.UserId!, new ListUserRolesRequestParameters { PerPage = 100 }, null, ct)
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 52.94118% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.14%. Comparing base (9c2b6ed) to head (d3e9a43).
⚠️ Report is 2 commits behind head on dev.

Files with missing lines Patch % Lines
.../Web/Features/Admin/Users/UserManagementService.cs 58.33% 11 Missing and 9 partials ⚠️
...b/Features/Admin/Users/UserManagementExtensions.cs 26.66% 10 Missing and 1 partial ⚠️
src/AppHost/AppHost.cs 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #266      +/-   ##
==========================================
- Coverage   78.52%   78.14%   -0.38%     
==========================================
  Files         228      228              
  Lines        8481     8462      -19     
  Branches     1166     1170       +4     
==========================================
- Hits         6660     6613      -47     
- Misses       1284     1305      +21     
- Partials      537      544       +7     
Files with missing lines Coverage Δ
src/AppHost/AppHost.cs 94.44% <80.00%> (-5.56%) ⬇️
...b/Features/Admin/Users/UserManagementExtensions.cs 45.00% <26.66%> (-55.00%) ⬇️
.../Web/Features/Admin/Users/UserManagementService.cs 54.33% <58.33%> (-8.91%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mpaulosky
Copy link
Copy Markdown
Owner Author

Lead triage: needs changes before this enters the review gate. The v8 migration is the right direction, but please reconcile Auth0Management:Audience (either wire it into the new client registration or remove the stale config/docs), harden ListUsersAsync against blank Auth0 user IDs, and restore explicit test assertions that cache-hit / empty-role paths do not call Auth0. The Azure Storage integration failure looks environmental, but this branch still needs to come back green; after that, route to Sam + Gandalf for backend/security review.

@mpaulosky
Copy link
Copy Markdown
Owner Author

🔒 Security review (Gandalf)

[MEDIUM] Blank/null Auth0 user IDs are still dereferenced via u.UserId! during role lookups in src/Web/Features/Admin/Users/UserManagementService.cs:107-111. If Auth0 ever returns a malformed user record, this turns the role-enrichment path into an unsafe null flow; please validate/fail fast before calling Users.Roles.ListAsync.

[INFO] Auth0Management:Audience is still documented in src/Web/Features/Admin/Users/Auth0ManagementOptions.cs:35-39 but no longer used by src/Web/Features/Admin/Users/UserManagementExtensions.cs:42-52. v8 appears to default correctly to https://{domain}/api/v2/, so this is config-drift rather than a blocker, but it should be reconciled to avoid future misconfiguration.

mpaulosky added a commit that referenced this pull request May 1, 2026
Add --skipApiVersionCheck flag to Azurite container command to support
Azure.Storage.Blobs v12.25.0 API version (2026-02-06) with Azurite 3.35.0.

This unblocks the pre-push gate for PR #266 by allowing the integration
test fixture to run successfully without API version validation errors.

The API version check is skipped in the test environment (Azurite) but
remains enabled in production (real Azure Storage).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mpaulosky added a commit that referenced this pull request May 1, 2026
…2026-04-01 entries

- Orchestration log: Boromir's Azurite API-version fix (commit 3d963fe)
- Session log: PR #266 pre-push gate unblock summary
- Decisions archive: Moved 678 pre-2026-04-01 lines to decisions-archive.md
- Decisions merge: Merged boromir-azurite-api-version-fix.md + aragorn-pr-triage.md into decisions.md
- Inbox cleanup: Removed merged decision files from decisions/inbox/
- Agent histories: Updated Boromir, Sam, Gandalf, Aragorn, Scribe with cross-team updates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mpaulosky and others added 3 commits April 30, 2026 19:22
…patibility

The Azure.Storage.Blobs v12.27.0 uses API version 2026-02-06, which is not
supported by Azurite 3.35.0 (max supported: 2025-11-05). Downgrading to
v12.20.0 uses an API version compatible with Azurite 3.35.0, allowing all
25 Azure Storage integration tests to pass without requiring the
--skipApiVersionCheck workaround.

Fixes: PR #266
All 25 Persistence.AzureStorage.Tests.Integration tests now pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mpaulosky added a commit that referenced this pull request May 1, 2026
- Updated Boromir history: Azure.Storage.Blobs v12.27.0 -> v12.20.0 downgrade task
- Updated Sam history: Next action for PR #266 integration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mpaulosky
Copy link
Copy Markdown
Owner Author

🔒 Security re-review (Gandalf) on 9e5383d

Approve-ready from security. The latest head wires Auth0Management:Audience back into ClientCredentialsTokenProvider, guards the list-user role enrichment path against blank UserId values before any Auth0 role lookup, and restores targeted assertions that cache-hit / empty-role paths do not call Auth0. I do not see any remaining Auth0/authorization regressions or least-privilege issues in the touched area.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 1, 2026 03:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the web app’s Auth0 Management API integration from v7 to v8 by switching from manual token acquisition to an injected IManagementApiClient, and updates supporting registrations/tests to match the new SDK patterns alongside broader NuGet package version updates.

Changes:

  • Bumped centralized NuGet package versions (including Auth0.ManagementApi v8.x and Aspire 13.2.4).
  • Refactored UserManagementService to use injected IManagementApiClient (v8) and updated role/user retrieval/assignment calls accordingly.
  • Updated unit/integration tests and test host configuration to remove old token-fetch mocking and accommodate v8 client requirements.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Directory.Packages.props Central package version updates (Auth0 v8, Aspire, MongoDB, testing deps, etc.).
src/Web/Features/Admin/Users/UserManagementService.cs Replaced manual M2M token flow with v8 IManagementApiClient usage; updated list/get/role operations and mapping.
src/Web/Features/Admin/Users/UserManagementExtensions.cs Registers IManagementApiClient singleton using ClientCredentialsTokenProvider.
tests/Web.Tests/Services/UserManagementServiceTests.cs Updates unit tests to stub IManagementApiClient instead of IHttpClientFactory token machinery; adds pager stub.
tests/Web.Tests/Features/Admin/Users/UserManagementServiceCacheTests.cs Updates cache-focused tests to use IManagementApiClient substitutes and removes HTTP handler/token tests.
tests/Web.Tests.Integration/CustomWebApplicationFactory.cs Adds Auth0Management config keys and stubs IUserManagementService in integration tests to avoid Auth0 dependencies.
src/AppHost/AppHost.cs Skips wiring /health HTTP health check in Testing to avoid readiness coupling during E2E runs.

Comment thread Directory.Packages.props
<PackageVersion Include="MongoDB.EntityFrameworkCore" Version="10.0.1" />
<!-- Azure Storage -->
<PackageVersion Include="Azure.Storage.Blobs" Version="12.25.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.20.0" />
Comment on lines 105 to +109
// Auth0's list endpoint does not include role assignments; fetch them per user in
// parallel to avoid sequential N+1 latency.
var summaries = await Task.WhenAll(users.Select(async u =>
{
var roles = await client.Users
.GetRolesAsync(u.UserId, new PaginationInfo(0, 100, false), ct)
if (string.IsNullOrWhiteSpace(u.UserId))
Comment on lines 29 to +33
/// These tests use a real <see cref="MemoryDistributedCache" /> so cache read/write round-trips
/// are exercised without mocking serialization internals. The Auth0 Management API layer is
/// replaced by a <see cref="FakeHttpMessageHandler" /> that returns precanned JSON for the
/// M2M token endpoint. Management API calls that would contact Auth0 are expected to fail with
/// <see cref="ResultErrorCode.ExternalService" /> — the tests assert cache behaviour, not the
/// success path of the Management API itself.
/// replaced by an NSubstitute <see cref="IManagementApiClient" /> stub. Cache-miss tests assert
/// <see cref="ResultErrorCode.ExternalService" /> (NSubstitute default returns a null-valued struct
/// which causes NullReferenceException, caught as ExternalService by the service).
Comment on lines 217 to +223
public async Task AssignRolesAsync_AfterSuccess_EvictsUserByIdCacheEntry()
{
// Arrange — pre-populate user-by-id cache, then simulate a successful role change by
// calling AssignRolesAsync through the real cache so the Remove() path executes.
// Because ManagementApiClient can't be fully mocked here we pre-call the service
// in a state where the role-assign fails at Auth0 (ExternalService), which means
// the eviction does NOT happen on that path. Instead we verify the eviction path
// directly by pre-populating and checking via AssignRolesAsync with empty roles
// (which returns early before any Auth0 call, so no eviction) vs observing that
// the successful AssignRolesAsync path calls Remove.
//
// Practical approach: use the empty-roles early-return path for non-eviction proof,
// and the direct distributed cache mock for eviction proof so the test stays pure.

// For the eviction tests we use a mock IDistributedCache so we can verify Remove calls.
// Arrange — use a mock IDistributedCache so we can verify Remove calls.
var memoryCache = new MemoryCache(new MemoryCacheOptions());
var distributedCache = Substitute.For<IDistributedCache>();
var logger = Substitute.For<ILogger<UserManagementService>>();
var factory = TokenOnlyHttpClientFactory();
var managementClient = Substitute.For<IManagementApiClient>();
Comment on lines 339 to +346
[Fact]
public async Task AssignRolesAsync_OnSuccess_CallsRemoveAsyncForUserByIdKey()
{
// NOTE: ManagementApiClient is sealed and constructs its own HttpClient, so the
// eviction-after-success path is not fully unit-testable without refactoring the
// service to accept an IManagementApiClientFactory. This test verifies that:
// a) the SUT accepts the injected IDistributedCache without null-ref,
// b) validation returns NotBe(Validation) for a valid non-empty roles call,
// c) a cache read error (sentinel path) does not surface as an exception.
// Full eviction coverage is tracked in TODO in UserManagementServiceTests.cs.
// NOTE: The Management API stub returns default (null-valued struct) which causes
// a NullReferenceException caught as ExternalService. The eviction path runs only
// after a confirmed success, so this test verifies the SUT wires up correctly and
// a non-empty roles call reaches Auth0 (returns ExternalService, not Validation).
// Full eviction coverage is tracked as a TODO in UserManagementServiceTests.cs.
Comment on lines 367 to 387
public async Task AssignRolesAsync_WhenDistributedCacheRemoveThrows_DoesNotRethrow()
{
// Arrange — RemoveAsync throws; the method should log and still return Ok after
// Auth0 succeeds (we simulate the post-commit eviction block).
var memoryCache = new MemoryCache(new MemoryCacheOptions());
var distributedCache = Substitute.For<IDistributedCache>();

// GetAsync returns null (cache miss)
distributedCache.GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns((byte[]?)null);

// RemoveAsync throws to simulate Redis failure
distributedCache
.RemoveAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns<Task>(_ => throw new InvalidOperationException("Redis unavailable"));

// Short-circuit: empty roles list returns Ok without hitting Auth0 or eviction path.
var factory = Substitute.For<IHttpClientFactory>();
// Empty roles list short-circuits before eviction — no throw expected.
var sut = new UserManagementService(
memoryCache, distributedCache, factory,
Options.Create(DefaultOptions),
memoryCache, distributedCache,
Substitute.For<IManagementApiClient>(),
Substitute.For<ILogger<UserManagementService>>());
Comment on lines 393 to 412
[Fact]
public async Task RemoveRolesAsync_WhenDistributedCacheRemoveThrows_DoesNotRethrow()
{
// Arrange — same pattern as AssignRolesAsync above.
var distributedCache = Substitute.For<IDistributedCache>();
distributedCache.GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns((byte[]?)null);
distributedCache
.RemoveAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns<Task>(_ => throw new InvalidOperationException("Redis unavailable"));

var sut = new UserManagementService(
new MemoryCache(new MemoryCacheOptions()),
distributedCache,
Substitute.For<IHttpClientFactory>(),
Options.Create(DefaultOptions),
Substitute.For<IManagementApiClient>(),
Substitute.For<ILogger<UserManagementService>>());

// Empty roles short-circuits before eviction — no throw expected.
var result = await sut.RemoveRolesAsync("auth0|u1", [], CancellationToken.None);
result.Success.Should().BeTrue();
@mpaulosky mpaulosky merged commit 9a80e14 into dev May 1, 2026
20 checks passed
@mpaulosky mpaulosky deleted the update/nuget-packages branch May 1, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squad Squad triage inbox — Lead will assign to a member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants