From 1e25f0fd1334006e98c62b12a4bff2a933c924d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:19:56 +0000 Subject: [PATCH 1/9] Initial plan From 62f5a6094b36a20260816f09b7ab38e7f904742d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:30:32 +0000 Subject: [PATCH 2/9] feat: refactor GrantService to use MediatR commands/queries in BusinessLogic Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/56b52efa-09c6-470d-9fa8-2b87a6f5f187 Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../RequestHandlers/GrantRequestHandler.cs | 41 +++--- .../Requests/SecurityServiceCommands.cs | 1 + .../Requests/SecurityServiceQueries.cs | 1 + .../Pages/GrantsPageModelTests.cs | 129 ++++++++++++++++++ .../GrantRequestHandlerTests.cs | 36 +++++ .../Pages/Account/Grants/Index.cshtml.cs | 20 +-- SecurityService/Program.cs | 2 - 7 files changed, 198 insertions(+), 32 deletions(-) rename SecurityService/Services/GrantService.cs => SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs (68%) create mode 100644 SecurityService.UnitTests/Pages/GrantsPageModelTests.cs create mode 100644 SecurityService.UnitTests/RequestHandlers/GrantRequestHandlerTests.cs diff --git a/SecurityService/Services/GrantService.cs b/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs similarity index 68% rename from SecurityService/Services/GrantService.cs rename to SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs index 25c2f3c6..21a30a1a 100644 --- a/SecurityService/Services/GrantService.cs +++ b/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs @@ -1,33 +1,28 @@ -using Microsoft.IdentityModel.Tokens; +using MediatR; using OpenIddict.Abstractions; +using SecurityService.BusinessLogic.Requests; using SecurityService.Models; using SimpleResults; -using System.IdentityModel.Tokens.Jwt; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace SecurityService.Services; +namespace SecurityService.BusinessLogic.RequestHandlers; -public interface IGrantService -{ - Task> GetUserGrantsAsync(string userId, CancellationToken cancellationToken); - - Task RevokeAsync(string userId, string authorizationId, CancellationToken cancellationToken); -} - -public sealed class GrantService : IGrantService +public sealed class GrantRequestHandler : + IRequestHandler>>, + IRequestHandler { private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly IOpenIddictApplicationManager _applicationManager; - public GrantService(IOpenIddictAuthorizationManager authorizationManager, IOpenIddictApplicationManager applicationManager) + public GrantRequestHandler(IOpenIddictAuthorizationManager authorizationManager, IOpenIddictApplicationManager applicationManager) { this._authorizationManager = authorizationManager; this._applicationManager = applicationManager; } - public async Task> GetUserGrantsAsync(string userId, CancellationToken cancellationToken) + public async Task>> Handle(SecurityServiceQueries.GetUserGrantsQuery query, CancellationToken cancellationToken) { - var authorizations = await this._authorizationManager.FindAsync(userId, client: null, status: Statuses.Valid, type: null, scopes: null, cancellationToken).ToListAsync(cancellationToken); + var authorizations = await this._authorizationManager.FindAsync(query.UserId, client: null, status: Statuses.Valid, type: null, scopes: null, cancellationToken).ToListAsync(cancellationToken); var grants = new List(); foreach (var authorization in authorizations) @@ -51,28 +46,30 @@ await this._authorizationManager.GetScopesAsync(authorization, cancellationToken await this._authorizationManager.GetCreationDateAsync(authorization, cancellationToken))); } - return grants + var sorted = grants .OrderByDescending(grant => grant.CreatedAt) .ThenBy(grant => grant.DisplayName, StringComparer.OrdinalIgnoreCase) - .ToArray(); + .ToList(); + + return Result.Success(sorted); } - public async Task RevokeAsync(string userId, string authorizationId, CancellationToken cancellationToken) + public async Task Handle(SecurityServiceCommands.RevokeGrantCommand command, CancellationToken cancellationToken) { - var authorization = await this._authorizationManager.FindByIdAsync(authorizationId, cancellationToken); + var authorization = await this._authorizationManager.FindByIdAsync(command.AuthorizationId, cancellationToken); if (authorization is null) { - return Result.NotFound($"No authorization found with id '{authorizationId}'."); + return Result.NotFound($"No authorization found with id '{command.AuthorizationId}'."); } var subject = await this._authorizationManager.GetSubjectAsync(authorization, cancellationToken); - if (string.Equals(subject, userId, StringComparison.Ordinal) == false) + if (string.Equals(subject, command.UserId, StringComparison.Ordinal) == false) { - return Result.NotFound($"No authorization found with id '{authorizationId}'."); + return Result.NotFound($"No authorization found with id '{command.AuthorizationId}'."); } return await this._authorizationManager.TryRevokeAsync(authorization, cancellationToken) ? Result.Success() : Result.Failure("The authorization could not be revoked."); } -} \ No newline at end of file +} diff --git a/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs b/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs index eca95f21..1ec4da65 100644 --- a/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs +++ b/SecurityService.BusinessLogic/Requests/SecurityServiceCommands.cs @@ -69,4 +69,5 @@ public record ProcessPasswordResetConfirmationCommand(String Username, String ClientId) : IRequest>; public sealed record LoginCommand(string Username, string Password, bool RememberLogin) : IRequest; + public sealed record RevokeGrantCommand(string UserId, string AuthorizationId) : IRequest; } diff --git a/SecurityService.BusinessLogic/Requests/SecurityServiceQueries.cs b/SecurityService.BusinessLogic/Requests/SecurityServiceQueries.cs index 0b8c54f1..7157df96 100644 --- a/SecurityService.BusinessLogic/Requests/SecurityServiceQueries.cs +++ b/SecurityService.BusinessLogic/Requests/SecurityServiceQueries.cs @@ -23,4 +23,5 @@ public sealed record GetRolesQuery() : IRequest>>; public sealed record GetUserQuery(string UserId) : IRequest>; public sealed record GetUsersQuery(string? UserName) : IRequest>>; public sealed record GetExternalProvidersQuery() : IRequest>>; + public sealed record GetUserGrantsQuery(string UserId) : IRequest>>; } diff --git a/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs b/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs new file mode 100644 index 00000000..0101b0a3 --- /dev/null +++ b/SecurityService.UnitTests/Pages/GrantsPageModelTests.cs @@ -0,0 +1,129 @@ +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Moq; +using SecurityService.BusinessLogic.Requests; +using SecurityService.Database; +using SecurityService.Models; +using SecurityService.UnitTests.Infrastructure; +using Shouldly; +using SimpleResults; + +namespace SecurityService.UnitTests.Pages; + +public class GrantsPageModelTests +{ + [Fact] + public async Task OnGetAsync_WhenUserNotFound_RedirectsToLogin() + { + var userManager = IdentityMocks.CreateUserManager(); + userManager.Setup(m => m.GetUserAsync(It.IsAny())) + .ReturnsAsync((ApplicationUser?)null); + + var mediator = new Mock(); + var model = CreateModel(userManager, mediator, new DefaultHttpContext()); + + var result = await model.OnGetAsync(CancellationToken.None); + + result.ShouldBeOfType(); + } + + [Fact] + public async Task OnGetAsync_WhenUserFound_QueriesGrantsAndReturnsPage() + { + var user = new ApplicationUser { Id = "user-1" }; + var userManager = IdentityMocks.CreateUserManager(); + userManager.Setup(m => m.GetUserAsync(It.IsAny())) + .ReturnsAsync(user); + + var grants = new List + { + new GrantDetails("auth-1", "client-1", "Client One", new[] { "openid" }, DateTimeOffset.UtcNow) + }; + + var mediator = new Mock(); + mediator.Setup(m => m.Send(It.Is(q => q.UserId == "user-1"), It.IsAny())) + .ReturnsAsync(Result.Success(grants)); + + var model = CreateModel(userManager, mediator, new DefaultHttpContext()); + + var result = await model.OnGetAsync(CancellationToken.None); + + result.ShouldBeOfType(); + model.Grants.ShouldHaveSingleItem(); + mediator.Verify(m => m.Send(It.Is(q => q.UserId == "user-1"), It.IsAny()), Times.Once); + } + + [Fact] + public async Task OnPostRevokeAsync_WhenUserNotFound_RedirectsToLogin() + { + var userManager = IdentityMocks.CreateUserManager(); + userManager.Setup(m => m.GetUserAsync(It.IsAny())) + .ReturnsAsync((ApplicationUser?)null); + + var mediator = new Mock(MockBehavior.Strict); + var model = CreateModel(userManager, mediator, new DefaultHttpContext()); + + var result = await model.OnPostRevokeAsync("auth-1", CancellationToken.None); + + result.ShouldBeOfType(); + mediator.Verify(m => m.Send(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task OnPostRevokeAsync_WhenRevokeSucceeds_RedirectsToPage() + { + var user = new ApplicationUser { Id = "user-1" }; + var userManager = IdentityMocks.CreateUserManager(); + userManager.Setup(m => m.GetUserAsync(It.IsAny())) + .ReturnsAsync(user); + + var mediator = new Mock(); + mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + + var model = CreateModel(userManager, mediator, new DefaultHttpContext()); + + var result = await model.OnPostRevokeAsync("auth-1", CancellationToken.None); + + result.ShouldBeOfType(); + } + + [Fact] + public async Task OnPostRevokeAsync_WhenRevokeFails_ReturnsPageWithStatusMessage() + { + var user = new ApplicationUser { Id = "user-1" }; + var userManager = IdentityMocks.CreateUserManager(); + userManager.Setup(m => m.GetUserAsync(It.IsAny())) + .ReturnsAsync(user); + + var mediator = new Mock(); + mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Failure("The authorization could not be revoked.")); + mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success(new List())); + + var model = CreateModel(userManager, mediator, new DefaultHttpContext()); + + var result = await model.OnPostRevokeAsync("auth-1", CancellationToken.None); + + result.ShouldBeOfType(); + model.StatusMessage.ShouldBe("The authorization could not be revoked."); + } + + private static SecurityService.Pages.Account.Grants.IndexModel CreateModel( + Mock> userManager, + Mock mediator, + HttpContext httpContext) + { + return new SecurityService.Pages.Account.Grants.IndexModel(userManager.Object, mediator.Object) + { + PageContext = new PageContext + { + HttpContext = httpContext + } + }; + } +} diff --git a/SecurityService.UnitTests/RequestHandlers/GrantRequestHandlerTests.cs b/SecurityService.UnitTests/RequestHandlers/GrantRequestHandlerTests.cs new file mode 100644 index 00000000..57322901 --- /dev/null +++ b/SecurityService.UnitTests/RequestHandlers/GrantRequestHandlerTests.cs @@ -0,0 +1,36 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using SecurityService.BusinessLogic.Requests; +using SecurityService.UnitTests.Infrastructure; +using Shouldly; +using SimpleResults; + +namespace SecurityService.UnitTests.RequestHandlers; + +public class GrantRequestHandlerTests +{ + [Fact] + public async Task GetUserGrants_WhenNoAuthorizations_ReturnsEmptyList() + { + using var provider = TestServiceProviderFactory.Create(nameof(this.GetUserGrants_WhenNoAuthorizations_ReturnsEmptyList)); + var mediator = provider.GetRequiredService(); + + var result = await mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery("user-1")); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ShouldNotBeNull(); + result.Data.ShouldBeEmpty(); + } + + [Fact] + public async Task RevokeGrant_WhenAuthorizationNotFound_ReturnsNotFound() + { + using var provider = TestServiceProviderFactory.Create(nameof(this.RevokeGrant_WhenAuthorizationNotFound_ReturnsNotFound)); + var mediator = provider.GetRequiredService(); + + var result = await mediator.Send(new SecurityServiceCommands.RevokeGrantCommand("user-1", "nonexistent-authorization-id")); + + result.IsFailed.ShouldBeTrue(); + result.Status.ShouldBe(ResultStatus.NotFound); + } +} diff --git a/SecurityService/Pages/Account/Grants/Index.cshtml.cs b/SecurityService/Pages/Account/Grants/Index.cshtml.cs index 132842b5..0dd585f5 100644 --- a/SecurityService/Pages/Account/Grants/Index.cshtml.cs +++ b/SecurityService/Pages/Account/Grants/Index.cshtml.cs @@ -1,22 +1,23 @@ +using MediatR; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using SecurityService.BusinessLogic.Oidc; +using SecurityService.BusinessLogic.Requests; using SecurityService.Database; using SecurityService.Models; -using SecurityService.BusinessLogic.Oidc; -using SecurityService.Services; namespace SecurityService.Pages.Account.Grants; public sealed class IndexModel : PageModel { private readonly UserManager _userManager; - private readonly IGrantService _grantService; + private readonly IMediator _mediator; - public IndexModel(UserManager userManager, IGrantService grantService) + public IndexModel(UserManager userManager, IMediator mediator) { this._userManager = userManager; - this._grantService = grantService; + this._mediator = mediator; } public IReadOnlyCollection Grants { get; private set; } = Array.Empty(); @@ -31,7 +32,8 @@ public async Task OnGetAsync(CancellationToken cancellationToken) return this.Redirect($"/Account/Login?returnUrl={Uri.EscapeDataString(OidcHelpers.BuildCurrentRequestUrl(this.Request))}"); } - this.Grants = await this._grantService.GetUserGrantsAsync(user.Id, cancellationToken); + var result = await this._mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery(user.Id), cancellationToken); + this.Grants = result.IsSuccess ? result.Data! : Array.Empty(); return this.Page(); } @@ -43,14 +45,16 @@ public async Task OnPostRevokeAsync(string authorizationId, Cance return this.Redirect($"/Account/Login?returnUrl={Uri.EscapeDataString(OidcHelpers.BuildCurrentRequestUrl(this.Request))}"); } - var result = await this._grantService.RevokeAsync(user.Id, authorizationId, cancellationToken); + var result = await this._mediator.Send(new SecurityServiceCommands.RevokeGrantCommand(user.Id, authorizationId), cancellationToken); if (result.IsSuccess == false) { this.StatusMessage = result.Message ?? "The grant could not be revoked."; - this.Grants = await this._grantService.GetUserGrantsAsync(user.Id, cancellationToken); + var grantsResult = await this._mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery(user.Id), cancellationToken); + this.Grants = grantsResult.IsSuccess ? grantsResult.Data! : Array.Empty(); return this.Page(); } return this.RedirectToPage(); } } + diff --git a/SecurityService/Program.cs b/SecurityService/Program.cs index dde2e84f..b001f21b 100644 --- a/SecurityService/Program.cs +++ b/SecurityService/Program.cs @@ -22,7 +22,6 @@ using SecurityService.HealthChecks; using SecurityService.HostedServices; using SecurityService.Oidc; -using SecurityService.Services; using Sentry.Extensibility; using Shared.Extensions; using Shared.General; @@ -216,7 +215,6 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddHostedService(); -builder.Services.AddScoped(); if (builder.Environment.IsEnvironment("IntegrationTest")) { builder.Services.AddHealthChecks(); From 4a8a7092bff5ffbdd560ff94ba7043a9e1ce08be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:33:59 +0000 Subject: [PATCH 3/9] fix: use null-coalescing instead of null-forgiving operator in Grants page model Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/56b52efa-09c6-470d-9fa8-2b87a6f5f187 Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- SecurityService/Pages/Account/Grants/Index.cshtml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SecurityService/Pages/Account/Grants/Index.cshtml.cs b/SecurityService/Pages/Account/Grants/Index.cshtml.cs index 0dd585f5..90b7a23e 100644 --- a/SecurityService/Pages/Account/Grants/Index.cshtml.cs +++ b/SecurityService/Pages/Account/Grants/Index.cshtml.cs @@ -33,7 +33,7 @@ public async Task OnGetAsync(CancellationToken cancellationToken) } var result = await this._mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery(user.Id), cancellationToken); - this.Grants = result.IsSuccess ? result.Data! : Array.Empty(); + this.Grants = result.Data ?? Array.Empty(); return this.Page(); } @@ -50,7 +50,7 @@ public async Task OnPostRevokeAsync(string authorizationId, Cance { this.StatusMessage = result.Message ?? "The grant could not be revoked."; var grantsResult = await this._mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery(user.Id), cancellationToken); - this.Grants = grantsResult.IsSuccess ? grantsResult.Data! : Array.Empty(); + this.Grants = grantsResult.Data ?? Array.Empty(); return this.Page(); } From 985638fa07120c342869ff42edcfb047df0df730 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:45:30 +0000 Subject: [PATCH 4/9] fix: extract BuildGrantDetailsAsync to reduce cyclomatic complexity in GrantRequestHandler Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/c0933733-1a73-46f8-ac36-c9fe73b5778e Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../RequestHandlers/GrantRequestHandler.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs b/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs index 21a30a1a..5abcf535 100644 --- a/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs +++ b/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs @@ -27,23 +27,11 @@ public async Task>> Handle(SecurityServiceQueries.GetU foreach (var authorization in authorizations) { - var authorizationId = await this._authorizationManager.GetIdAsync(authorization, cancellationToken); - if (string.IsNullOrWhiteSpace(authorizationId)) + var grant = await this.BuildGrantDetailsAsync(authorization, cancellationToken); + if (grant is not null) { - continue; + grants.Add(grant); } - - var applicationId = await this._authorizationManager.GetApplicationIdAsync(authorization, cancellationToken); - object? application = string.IsNullOrWhiteSpace(applicationId) ? null : await this._applicationManager.FindByIdAsync(applicationId, cancellationToken); - var clientId = application is null ? string.Empty : await this._applicationManager.GetClientIdAsync(application, cancellationToken) ?? string.Empty; - var displayName = application is null ? clientId : await this._applicationManager.GetDisplayNameAsync(application, cancellationToken) ?? clientId; - - grants.Add(new GrantDetails( - authorizationId, - clientId, - string.IsNullOrWhiteSpace(displayName) ? clientId : displayName, - await this._authorizationManager.GetScopesAsync(authorization, cancellationToken), - await this._authorizationManager.GetCreationDateAsync(authorization, cancellationToken))); } var sorted = grants @@ -54,6 +42,27 @@ await this._authorizationManager.GetScopesAsync(authorization, cancellationToken return Result.Success(sorted); } + private async Task BuildGrantDetailsAsync(object authorization, CancellationToken cancellationToken) + { + var authorizationId = await this._authorizationManager.GetIdAsync(authorization, cancellationToken); + if (string.IsNullOrWhiteSpace(authorizationId)) + { + return null; + } + + var applicationId = await this._authorizationManager.GetApplicationIdAsync(authorization, cancellationToken); + object? application = string.IsNullOrWhiteSpace(applicationId) ? null : await this._applicationManager.FindByIdAsync(applicationId, cancellationToken); + var clientId = application is null ? string.Empty : await this._applicationManager.GetClientIdAsync(application, cancellationToken) ?? string.Empty; + var displayName = application is null ? clientId : await this._applicationManager.GetDisplayNameAsync(application, cancellationToken) ?? clientId; + + return new GrantDetails( + authorizationId, + clientId, + string.IsNullOrWhiteSpace(displayName) ? clientId : displayName, + await this._authorizationManager.GetScopesAsync(authorization, cancellationToken), + await this._authorizationManager.GetCreationDateAsync(authorization, cancellationToken)); + } + public async Task Handle(SecurityServiceCommands.RevokeGrantCommand command, CancellationToken cancellationToken) { var authorization = await this._authorizationManager.FindByIdAsync(command.AuthorizationId, cancellationToken); From 9ff2dc7273a1f4da59d69c428f3837a546376063 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:55:24 +0000 Subject: [PATCH 5/9] fix: remove stale using SecurityService.Services from DeveloperHandler and ForgotPassword Index Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/4bee2cf7-c495-4894-b9dd-7effb86cfda4 Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- SecurityService/Handlers/DeveloperHandler.cs | 1 - SecurityService/Pages/Account/ForgotPassword/Index.cshtml.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/SecurityService/Handlers/DeveloperHandler.cs b/SecurityService/Handlers/DeveloperHandler.cs index bd2f8095..ffaf523c 100644 --- a/SecurityService/Handlers/DeveloperHandler.cs +++ b/SecurityService/Handlers/DeveloperHandler.cs @@ -1,7 +1,6 @@ using MessagingService.Client; using MessagingService.DataTransferObjects; using SecurityService.BusinessLogic; -using SecurityService.Services; namespace SecurityService.Handlers; diff --git a/SecurityService/Pages/Account/ForgotPassword/Index.cshtml.cs b/SecurityService/Pages/Account/ForgotPassword/Index.cshtml.cs index b0faf689..f8dc5c4b 100644 --- a/SecurityService/Pages/Account/ForgotPassword/Index.cshtml.cs +++ b/SecurityService/Pages/Account/ForgotPassword/Index.cshtml.cs @@ -6,7 +6,6 @@ using SecurityService.BusinessLogic.Requests; using SecurityService.Database; using SecurityService.Pages.Account.ChangePassword; -using SecurityService.Services; using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Text; From 6af3e798d76e1be7a5b909007dd11ef9678fc566 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:02:14 +0000 Subject: [PATCH 6/9] fix: remove remaining stale using SecurityService.Services references Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/2ffdd193-6494-4fbe-9aee-d284bc4c489f Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- SecurityService.UnitTests/Handlers/DeveloperHandlerTests.cs | 1 - SecurityService.UnitTests/Infrastructure/IdentityMocks.cs | 1 - .../Pages/Account/ResendConfirmationEmail/Index.cshtml.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/SecurityService.UnitTests/Handlers/DeveloperHandlerTests.cs b/SecurityService.UnitTests/Handlers/DeveloperHandlerTests.cs index 69224619..5bf46d1e 100644 --- a/SecurityService.UnitTests/Handlers/DeveloperHandlerTests.cs +++ b/SecurityService.UnitTests/Handlers/DeveloperHandlerTests.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http.HttpResults; using SecurityService.BusinessLogic; using SecurityService.Handlers; -using SecurityService.Services; using Shouldly; namespace SecurityService.UnitTests.Handlers; diff --git a/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs b/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs index dbb86eb8..855458bc 100644 --- a/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs +++ b/SecurityService.UnitTests/Infrastructure/IdentityMocks.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using SecurityService.Database; -using SecurityService.Services; namespace SecurityService.UnitTests.Infrastructure; diff --git a/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs b/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs index bb2c2c35..9db216e4 100644 --- a/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs +++ b/SecurityService/Pages/Account/ResendConfirmationEmail/Index.cshtml.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; using SecurityService.Database; -using SecurityService.Services; namespace SecurityService.Pages.Account.ResendConfirmationEmail; From aa1800f7109f53e7e1a2f99c039bb22df004ad60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:08:33 +0000 Subject: [PATCH 7/9] fix: re-include Grants page in compilation (remove Compile/Content/Folder Remove exclusion) Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/446bc522-d6cd-4ed7-8c28-af9162eb814e Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- SecurityService/SecurityService.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/SecurityService/SecurityService.csproj b/SecurityService/SecurityService.csproj index 753d4275..e9da776f 100644 --- a/SecurityService/SecurityService.csproj +++ b/SecurityService/SecurityService.csproj @@ -7,12 +7,10 @@ - - @@ -70,7 +68,6 @@ - From a99e6b62339ef12ebdbe6a804646d7b2b8d0bce3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:13:42 +0000 Subject: [PATCH 8/9] fix: use List fallback in Grants page (CS0019) Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/30e75bb9-e03f-4316-8eb1-6aaf9f516ad3 Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- SecurityService/Pages/Account/Grants/Index.cshtml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SecurityService/Pages/Account/Grants/Index.cshtml.cs b/SecurityService/Pages/Account/Grants/Index.cshtml.cs index 90b7a23e..6720e83a 100644 --- a/SecurityService/Pages/Account/Grants/Index.cshtml.cs +++ b/SecurityService/Pages/Account/Grants/Index.cshtml.cs @@ -33,7 +33,7 @@ public async Task OnGetAsync(CancellationToken cancellationToken) } var result = await this._mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery(user.Id), cancellationToken); - this.Grants = result.Data ?? Array.Empty(); + this.Grants = result.Data ?? new List(); return this.Page(); } @@ -50,7 +50,7 @@ public async Task OnPostRevokeAsync(string authorizationId, Cance { this.StatusMessage = result.Message ?? "The grant could not be revoked."; var grantsResult = await this._mediator.Send(new SecurityServiceQueries.GetUserGrantsQuery(user.Id), cancellationToken); - this.Grants = grantsResult.Data ?? Array.Empty(); + this.Grants = grantsResult.Data ?? new List(); return this.Page(); } From ea183fe96e61278c2578f38624f404054ed4f91f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:36:14 +0000 Subject: [PATCH 9/9] refactor: extract GetApplicationDisplayAsync to reduce cyclomatic complexity Agent-Logs-Url: https://github.com/TransactionProcessing/SecurityService/sessions/120d7502-4b83-4516-9ee0-bec924353d8f Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../RequestHandlers/GrantRequestHandler.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs b/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs index 5abcf535..febfd2b5 100644 --- a/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs +++ b/SecurityService.BusinessLogic/RequestHandlers/GrantRequestHandler.cs @@ -51,18 +51,34 @@ public async Task>> Handle(SecurityServiceQueries.GetU } var applicationId = await this._authorizationManager.GetApplicationIdAsync(authorization, cancellationToken); - object? application = string.IsNullOrWhiteSpace(applicationId) ? null : await this._applicationManager.FindByIdAsync(applicationId, cancellationToken); - var clientId = application is null ? string.Empty : await this._applicationManager.GetClientIdAsync(application, cancellationToken) ?? string.Empty; - var displayName = application is null ? clientId : await this._applicationManager.GetDisplayNameAsync(application, cancellationToken) ?? clientId; + var (clientId, displayName) = await this.GetApplicationDisplayAsync(applicationId, cancellationToken); return new GrantDetails( authorizationId, clientId, - string.IsNullOrWhiteSpace(displayName) ? clientId : displayName, + displayName, await this._authorizationManager.GetScopesAsync(authorization, cancellationToken), await this._authorizationManager.GetCreationDateAsync(authorization, cancellationToken)); } + private async Task<(string clientId, string displayName)> GetApplicationDisplayAsync(string? applicationId, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(applicationId)) + { + return (string.Empty, string.Empty); + } + + var application = await this._applicationManager.FindByIdAsync(applicationId, cancellationToken); + if (application is null) + { + return (string.Empty, string.Empty); + } + + var clientId = await this._applicationManager.GetClientIdAsync(application, cancellationToken) ?? string.Empty; + var displayName = await this._applicationManager.GetDisplayNameAsync(application, cancellationToken) ?? clientId; + return (clientId, string.IsNullOrWhiteSpace(displayName) ? clientId : displayName); + } + public async Task Handle(SecurityServiceCommands.RevokeGrantCommand command, CancellationToken cancellationToken) { var authorization = await this._authorizationManager.FindByIdAsync(command.AuthorizationId, cancellationToken);