From fee696814495ae2a92e1f7d3cb8b8fddc50bb635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 08:37:02 +0100 Subject: [PATCH 01/11] Refactor CreateRoleAssignmentEndpoint --- .../CreateRoleAssignmentEndpoint.cs | 42 +++++++------------ .../Extensions/ServiceCollectionExtensions.cs | 11 +++++ .../Repositories/RoleAssignmentRepository.cs | 14 ++++++- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs index a3eaa6c5..39f134bf 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs @@ -30,16 +30,8 @@ internal sealed class CreateRoleAssignmentEndpoint : EndpointBase Handle( [FromBody] CreateRoleAssignmentEndpointRequest request, - IApiKeyRepository apiKeyRepository, - IFolderRepository folderRepository, - IImageRepository imageRepository, - IOrganizationRepository organizationRepository, - IPlanningRealmRepository planningRealmRepository, - ITournamentRepository tournamentRepository, - IUserRepository userRepository, - IVenueRepository venueRepository, - IAccessValidator accessValidator, IServiceProvider serviceProvider, + IAccessValidator accessValidator, IMapper mapper, CancellationToken cancellationToken) { @@ -55,13 +47,13 @@ private static async Task Handle( var task = typeName switch { - "ApiKey" => CreateRoleAssignmentAsync(request, apiKeyRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Folder" => CreateRoleAssignmentAsync(request, folderRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Image" => CreateRoleAssignmentAsync(request, imageRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Organization" => CreateRoleAssignmentAsync(request, organizationRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "PlanningRealm" => CreateRoleAssignmentAsync(request, planningRealmRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Tournament" => CreateRoleAssignmentAsync(request, tournamentRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), - "Venue" => CreateRoleAssignmentAsync(request, venueRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "ApiKey" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Folder" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Image" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Organization" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "PlanningRealm" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Tournament" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), + "Venue" => CreateRoleAssignmentAsync(request, serviceProvider, accessValidator, mapper, targetId, cancellationToken), _ => null }; @@ -72,16 +64,14 @@ private static async Task Handle( private static async Task CreateRoleAssignmentAsync( CreateRoleAssignmentEndpointRequest request, - IRepositoryWithPublicId repository, - PublicId targetId, + IServiceProvider serviceProvider, IAccessValidator accessValidator, - IApiKeyRepository apiKeyRepository, - IUserRepository userRepository, - IRoleAssignmentRepository roleAssignmentRepository, IMapper mapper, + PublicId targetId, CancellationToken cancellationToken) where T : Entity, IEntityWithRoleAssignments { + var repository = serviceProvider.GetRequiredService>(); var entity = await repository.GetByPublicIdAsync(targetId); if (entity is null) @@ -94,7 +84,7 @@ private static async Task CreateRoleAssignmentAsync( return Results.Forbid(); } - var principal = await GetPrincipalAsync(request, apiKeyRepository, userRepository); + var principal = await GetPrincipalAsync(request, serviceProvider); if (principal is null) { @@ -106,6 +96,7 @@ private static async Task CreateRoleAssignmentAsync( return Results.Conflict("There already exists a role assignment for the specified principal/role combination."); } + var roleAssignmentRepository = serviceProvider.GetRequiredService>(); var roleAssignment = entity.AddRoleAssignment(request.Role, principal); await roleAssignmentRepository.CreateAsync(roleAssignment); @@ -114,13 +105,11 @@ private static async Task CreateRoleAssignmentAsync( return Results.Ok(mapper.Map(roleAssignment)); } - private static async Task GetPrincipalAsync( - CreateRoleAssignmentEndpointRequest request, - IApiKeyRepository apiKeyRepository, - IUserRepository userRepository) + private static async Task GetPrincipalAsync(CreateRoleAssignmentEndpointRequest request, IServiceProvider serviceProvider) { if (request.ApiKeyId.HasValue) { + var apiKeyRepository = serviceProvider.GetRequiredService(); var apiKey = await apiKeyRepository.GetByPublicIdAsync(request.ApiKeyId.Value); return apiKey?.AsPrincipal(); @@ -128,6 +117,7 @@ private static async Task CreateRoleAssignmentAsync( if (request.UserNameOrEmail is not null) { + var userRepository = serviceProvider.GetRequiredService(); var user = await userRepository.GetByUserNameOrEmailAsync(request.UserNameOrEmail); return user?.AsPrincipal(); diff --git a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs index 0660ece7..faea4326 100644 --- a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Document; using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; @@ -59,6 +60,16 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped(); services.AddScoped(); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped>(sp => sp.GetRequiredService()); + services.AddScoped, ApiKeyRoleAssignmentRepository>(); services.AddScoped, FolderRoleAssignmentRepository>(); services.AddScoped, ImageRoleAssignmentRepository>(); diff --git a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs index 4eb3dc0a..83a53c36 100644 --- a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs +++ b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore; using Turnierplan.Core.ApiKey; using Turnierplan.Core.Entity; using Turnierplan.Core.Folder; @@ -11,10 +12,19 @@ namespace Turnierplan.Dal.Repositories; public interface IRoleAssignmentRepository : IRepository, Guid> - where T : Entity, IEntityWithRoleAssignments; + where T : Entity, IEntityWithRoleAssignments +{ + Task RemoveByPrincipalAsync(Principal principal); +} internal abstract class RoleAssignmentRepositoryBase(TurnierplanContext context) : RepositoryBase, Guid>(context), IRoleAssignmentRepository - where T : Entity, IEntityWithRoleAssignments; + where T : Entity, IEntityWithRoleAssignments +{ + public async Task RemoveByPrincipalAsync(Principal principal) + { + await DbSet.Where(x => x.Principal == principal).ExecuteDeleteAsync(); + } +} internal sealed class ApiKeyRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context); From 9149488cc48fd54b1a440e4e4c778b3b28ee53ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 08:39:07 +0100 Subject: [PATCH 02/11] Refactor GetRoleAssignmentsEndpoint --- .../GetRoleAssignmentsEndpoint.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs index c05f5c39..2377d317 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs @@ -3,9 +3,15 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; using Turnierplan.Core.Entity; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.PlanningRealm; using Turnierplan.Core.PublicId; using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.RoleAssignments; @@ -20,13 +26,7 @@ internal sealed class GetRoleAssignmentsEndpoint : EndpointBase Handle( [FromRoute] string scopeId, - IApiKeyRepository apiKeyRepository, - IFolderRepository folderRepository, - IImageRepository imageRepository, - IOrganizationRepository organizationRepository, - IPlanningRealmRepository planningRealmRepository, - ITournamentRepository tournamentRepository, - IVenueRepository venueRepository, + IServiceProvider serviceProvider, IAccessValidator accessValidator, IMapper mapper) { @@ -37,13 +37,13 @@ private static async Task Handle( var task = typeName switch { - "ApiKey" => GetRoleAssignmentsAsync(apiKeyRepository, targetId, accessValidator, mapper), - "Folder" => GetRoleAssignmentsAsync(folderRepository, targetId, accessValidator, mapper), - "Image" => GetRoleAssignmentsAsync(imageRepository, targetId, accessValidator, mapper), - "Organization" => GetRoleAssignmentsAsync(organizationRepository, targetId, accessValidator, mapper), - "PlanningRealm" => GetRoleAssignmentsAsync(planningRealmRepository, targetId, accessValidator, mapper), - "Tournament" => GetRoleAssignmentsAsync(tournamentRepository, targetId, accessValidator, mapper), - "Venue" => GetRoleAssignmentsAsync(venueRepository, targetId, accessValidator, mapper), + "ApiKey" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Folder" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Image" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Organization" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "PlanningRealm" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Tournament" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), + "Venue" => GetRoleAssignmentsAsync(serviceProvider, accessValidator, mapper, targetId), _ => null }; @@ -52,9 +52,10 @@ private static async Task Handle( : await task; } - private static async Task GetRoleAssignmentsAsync(IRepositoryWithPublicId repository, PublicId targetId, IAccessValidator accessValidator, IMapper mapper) + private static async Task GetRoleAssignmentsAsync(IServiceProvider serviceProvider, IAccessValidator accessValidator, IMapper mapper, PublicId targetId) where T : Entity, IEntityWithRoleAssignments { + var repository = serviceProvider.GetRequiredService>(); var entity = await repository.GetByPublicIdAsync(targetId); if (entity is null) From 22c800ad4b7b8ff584c22889a5c22d67a7a019b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 08:40:26 +0100 Subject: [PATCH 03/11] Refactor DeleteRoleAssignmentEndpoint --- .../DeleteRoleAssignmentEndpoint.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs index bd739911..795d7efb 100644 --- a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs @@ -1,10 +1,16 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Helpers; using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; using Turnierplan.Core.Entity; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; using Turnierplan.Core.Organization; +using Turnierplan.Core.PlanningRealm; using Turnierplan.Core.PublicId; using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.RoleAssignments; @@ -20,13 +26,7 @@ internal sealed class DeleteRoleAssignmentEndpoint : EndpointBase private static async Task Handle( [FromRoute] string scopeId, [FromRoute] string roleAssignmentId, - IApiKeyRepository apiKeyRepository, - IFolderRepository folderRepository, - IImageRepository imageRepository, - IOrganizationRepository organizationRepository, - IPlanningRealmRepository planningRealmRepository, - ITournamentRepository tournamentRepository, - IVenueRepository venueRepository, + IServiceProvider serviceProvider, IAccessValidator accessValidator, CancellationToken cancellationToken) { @@ -42,13 +42,13 @@ private static async Task Handle( var task = typeName switch { - "ApiKey" => DeleteRoleAssignmentAsync(apiKeyRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Folder" => DeleteRoleAssignmentAsync(folderRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Image" => DeleteRoleAssignmentAsync(imageRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Organization" => DeleteRoleAssignmentAsync(organizationRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "PlanningRealm" => DeleteRoleAssignmentAsync(planningRealmRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Tournament" => DeleteRoleAssignmentAsync(tournamentRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), - "Venue" => DeleteRoleAssignmentAsync(venueRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "ApiKey" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Folder" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Image" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Organization" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "PlanningRealm" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Tournament" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), + "Venue" => DeleteRoleAssignmentAsync(serviceProvider, accessValidator, targetId, roleAssignmentGuid, cancellationToken), _ => null }; @@ -58,13 +58,14 @@ private static async Task Handle( } private static async Task DeleteRoleAssignmentAsync( - IRepositoryWithPublicId repository, - PublicId targetId, + IServiceProvider serviceProvider, IAccessValidator accessValidator, + PublicId targetId, Guid roleAssignmentId, CancellationToken cancellationToken) where T : Entity, IEntityWithRoleAssignments { + var repository = serviceProvider.GetRequiredService>(); var entity = await repository.GetByPublicIdAsync(targetId); if (entity is null) From 738851d5d661a9be235b5c216d1f542c0017b32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 08:49:12 +0100 Subject: [PATCH 04/11] Fix bug --- .../Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs | 9 +++++++++ .../Endpoints/Users/DeleteUserEndpoint.cs | 9 +++++++++ .../Extensions/ServiceCollectionExtensions.cs | 8 ++++++++ .../Repositories/RoleAssignmentRepository.cs | 13 ++++++++----- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs index 36eedf3c..ab1a16d6 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Security; +using Turnierplan.Core.Extensions; using Turnierplan.Core.PublicId; using Turnierplan.Dal.Repositories; @@ -17,6 +18,7 @@ private static async Task Handle( [FromRoute] PublicId id, IApiKeyRepository repository, IAccessValidator accessValidator, + IServiceProvider serviceProvider, CancellationToken cancellationToken) { var apiKey = await repository.GetByPublicIdAsync(id); @@ -33,6 +35,13 @@ private static async Task Handle( repository.Remove(apiKey); + var principal = apiKey.AsPrincipal(); + + foreach (var roleAssignmentRepository in serviceProvider.GetServices()) + { + await roleAssignmentRepository.RemoveAllByPrincipalAsync(principal); + } + await repository.UnitOfWork.SaveChangesAsync(cancellationToken); return Results.NoContent(); diff --git a/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs b/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs index f1a089e3..87ffeff4 100644 --- a/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Users/DeleteUserEndpoint.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Extensions; +using Turnierplan.Core.Extensions; using Turnierplan.Dal.Repositories; namespace Turnierplan.App.Endpoints.Users; @@ -18,6 +19,7 @@ private static async Task Handle( [FromRoute] Guid id, HttpContext context, IUserRepository repository, + IServiceProvider serviceProvider, CancellationToken cancellationToken) { if (id == context.GetCurrentUserIdOrThrow()) @@ -34,6 +36,13 @@ private static async Task Handle( repository.Remove(user); + var principal = user.AsPrincipal(); + + foreach (var roleAssignmentRepository in serviceProvider.GetServices()) + { + await roleAssignmentRepository.RemoveAllByPrincipalAsync(principal); + } + await repository.UnitOfWork.SaveChangesAsync(cancellationToken); return Results.NoContent(); diff --git a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs index faea4326..4c05efa8 100644 --- a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs @@ -77,5 +77,13 @@ public static void AddTurnierplanDataAccessLayer(this IServiceCollection service services.AddScoped, PlanningRealmRoleAssignmentRepository>(); services.AddScoped, TournamentRoleAssignmentRepository>(); services.AddScoped, VenueRoleAssignmentRepository>(); + + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); + services.AddScoped(sp => sp.GetRequiredService>()); } } diff --git a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs index 83a53c36..3b959770 100644 --- a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs +++ b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepository.cs @@ -11,18 +11,21 @@ namespace Turnierplan.Dal.Repositories; -public interface IRoleAssignmentRepository : IRepository, Guid> - where T : Entity, IEntityWithRoleAssignments +public interface IRoleAssignmentRepository { - Task RemoveByPrincipalAsync(Principal principal); + Task RemoveAllByPrincipalAsync(Principal principal); } +public interface IRoleAssignmentRepository : IRepository, Guid>, IRoleAssignmentRepository + where T : Entity, IEntityWithRoleAssignments; + internal abstract class RoleAssignmentRepositoryBase(TurnierplanContext context) : RepositoryBase, Guid>(context), IRoleAssignmentRepository where T : Entity, IEntityWithRoleAssignments { - public async Task RemoveByPrincipalAsync(Principal principal) + public async Task RemoveAllByPrincipalAsync(Principal principal) { - await DbSet.Where(x => x.Principal == principal).ExecuteDeleteAsync(); + var roleAssignments = await DbSet.Where(x => x.Principal == principal).ToListAsync(); + DbSet.RemoveRange(roleAssignments); } } From 809ba3c2ca367eed3029a0dfd442daf351cd9a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 08:53:52 +0100 Subject: [PATCH 05/11] Update display in UI --- src/Turnierplan.App/Client/src/app/i18n/de.ts | 5 ++++- .../rbac-offcanvas/rbac-offcanvas.component.html | 15 ++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index e6c9b92a..c9f43a0e 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -1390,7 +1390,10 @@ export const de = { User: 'Benutzer' }, PrincipalNotFound: 'Der Prinzipal konnte nicht mehr gefunden werden. Wurde der Nutzer/API-Schlüssel gelöscht?', - TotalCount: '{{count}} Zuweisungen', + TotalCount: { + One: '1 Zuweisung', + Many: '{{count}} Zuweisungen' + }, Id: 'ID:', CreatedAt: 'Erstellt am:', Inherited: 'Vererbt durch:', diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html index 35886acc..c40ec0f3 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html @@ -26,11 +26,16 @@ (buttonClick)="showAddRoleAssignmentDialog()" /> - @if (roleAssignmentCount > 3) { -
- -
- } +
+ @if (roleAssignmentCount === 1) { + + } @else { + + } +
From c46bda33b17eb14f697ba91e8a92bc0f9aa35310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 09:03:30 +0100 Subject: [PATCH 06/11] Some delete-related cleanups --- .../Client/e2e/consts/turnierplan.ts | 4 ++- .../e2e/pages/view-organization-page.ts | 2 +- src/Turnierplan.App/Client/src/app/i18n/de.ts | 13 ++++--- .../delete-modal/delete-modal.component.html | 6 ++-- .../labels-manager.component.html | 27 +++----------- .../labels-manager.component.ts | 36 +++++-------------- 6 files changed, 28 insertions(+), 60 deletions(-) diff --git a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts index 21c5d6cc..b4fbdd34 100644 --- a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts +++ b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts @@ -4,10 +4,12 @@ export const turnierplan = { organizationNameField: 'create-organization-page-organization-name-field' }, deleteWidget: { - confirmDeleteButton: 'delete-widget-confirm-delete-button', deleteButton: 'delete-widget-delete-button', confirmationField: 'delete-widget-confirmation-field' }, + deleteModal: { + confirmDeleteButton: 'delete-modal-confirm-delete-button' + }, header: { logoLink: 'header-logo-link' }, diff --git a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts index f5d69621..73ba5271 100644 --- a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts +++ b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts @@ -15,6 +15,6 @@ export class ViewOrganizationPage { await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.settingsPageId)).click(); await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(confirmText); await this.page.getByTestId(turnierplan.deleteWidget.deleteButton).click(); - await this.page.getByTestId(turnierplan.deleteWidget.confirmDeleteButton).click(); + await this.page.getByTestId(turnierplan.deleteModal.confirmDeleteButton).click(); } } diff --git a/src/Turnierplan.App/Client/src/app/i18n/de.ts b/src/Turnierplan.App/Client/src/app/i18n/de.ts index c9f43a0e..5ffc1f30 100644 --- a/src/Turnierplan.App/Client/src/app/i18n/de.ts +++ b/src/Turnierplan.App/Client/src/app/i18n/de.ts @@ -1116,9 +1116,10 @@ export const de = { Description: 'Beschreibung', NoLabels: 'Es sind aktuell keine Labels vorhanden.', LabelsInfo: 'Labels können verwendet werden, um angemeldete Mannschaften zu kategorisieren und zu filtern.', - DeleteWarning: { + Delete: { Title: 'Label löschen', - Text: 'Wenn Sie ein Label löschen, wird dieses Label von allen Mannschaften entfernt, bei denen dieses Label aktuell zugewiesen ist. Dies kann nicht rückgängig gemacht werden!' + AdditionalModalText: + 'Wenn Sie ein Label löschen, wird dieses Label von allen Mannschaften entfernt, bei denen dieses Label aktuell zugewiesen ist. Dies kann nicht rückgängig gemacht werden!' } }, SaveToViewApplications: 'Speichern Sie die offenen Änderungen, um die Anmeldungen zu sehen und neue Anmeldungen hinzuzufügen.', @@ -1306,9 +1307,11 @@ export const de = { DeleteWidget: { EnterToConfirm: 'Zur Bestätigung geben Sie bitte "{{text}}" in folgendes Textfeld ein:', - ConfirmModalText: 'Bestätigen Sie den Löschvorgang. Dies kann nicht rückgängig gemacht werden!', - Delete: 'Löschen', - DeleteConfirm: 'Löschen bestätigen' + Delete: 'Löschen' + }, + DeleteModal: { + InfoText: 'Bestätigen Sie den Löschvorgang. Dies kann nicht rückgängig gemacht werden!', + Confirm: 'Löschen bestätigen' }, VisibilitySelector: { Private: 'Privat', diff --git a/src/Turnierplan.App/Client/src/app/portal/components/delete-modal/delete-modal.component.html b/src/Turnierplan.App/Client/src/app/portal/components/delete-modal/delete-modal.component.html index 56fa78b5..e7415695 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/delete-modal/delete-modal.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/delete-modal/delete-modal.component.html @@ -2,7 +2,7 @@ } @@ -95,20 +95,3 @@ } - - - - - - diff --git a/src/Turnierplan.App/Client/src/app/portal/components/labels-manager/labels-manager.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/labels-manager/labels-manager.component.ts index 2c571645..a4f8fc8a 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/labels-manager/labels-manager.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/labels-manager/labels-manager.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { PlanningRealmDto } from '../../../api/models/planning-realm-dto'; import { UpdatePlanningRealmFunc, ViewPlanningRealmComponent } from '../../pages/view-planning-realm/view-planning-realm.component'; import { ApplicationsFilter } from '../../models/applications-filter'; @@ -10,7 +10,8 @@ import { LabelComponent } from '../label/label.component'; import { RenameButtonComponent } from '../rename-button/rename-button.component'; import { ActionButtonComponent } from '../action-button/action-button.component'; import { IsActionAllowedDirective } from '../../directives/is-action-allowed.directive'; -import { NgbModal, NgbModalRef, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { DeleteButtonComponent } from '../delete-button/delete-button.component'; @Component({ selector: 'tp-labels-manager', @@ -21,7 +22,8 @@ import { NgbModal, NgbModalRef, NgbPopoverModule } from '@ng-bootstrap/ng-bootst RenameButtonComponent, ActionButtonComponent, IsActionAllowedDirective, - NgbPopoverModule + NgbPopoverModule, + DeleteButtonComponent ], templateUrl: './labels-manager.component.html' }) @@ -38,13 +40,7 @@ export class LabelsManagerComponent { protected readonly Actions = Actions; protected readonly availableColors = ViewPlanningRealmComponent.DefaultLabelColorCodes; - protected confirmDeleteModal?: NgbModalRef; - protected currentDeletingLabelId?: number; - - constructor( - protected readonly authorizationService: AuthorizationService, - private readonly modalService: NgbModal - ) {} + constructor(protected readonly authorizationService: AuthorizationService) {} protected setLabelName(id: number, name: string): void { this.updatePlanningRealm((planningRealm) => { @@ -89,22 +85,9 @@ export class LabelsManagerComponent { }); } - protected deleteLabel(template: TemplateRef, id: number): void { - this.currentDeletingLabelId = id; - this.confirmDeleteModal = this.modalService.open(template, { - size: 'md', - fullscreen: 'md', - centered: true - }); - } - - protected confirmDeleteClicked(): void { - if (!this.currentDeletingLabelId) { - return; - } - + protected deleteLabel(id: number): void { this.updatePlanningRealm((planningRealm) => { - const index = planningRealm.labels.findIndex((x) => x.id === this.currentDeletingLabelId); + const index = planningRealm.labels.findIndex((x) => x.id === id); if (index === -1) { return false; @@ -114,9 +97,6 @@ export class LabelsManagerComponent { return true; }); - - this.confirmDeleteModal?.close(); - this.currentDeletingLabelId = undefined; } protected searchApplicationsClicked(id: number): void { From 5411a7a33a4112d9bd79f60a66cecc6af984b8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 09:45:59 +0100 Subject: [PATCH 07/11] Start working on e2e test, one todo is still open --- .../Client/e2e/consts/turnierplan.ts | 16 ++++++++- .../Client/e2e/pages/rbac-offcanvas.ts | 14 ++++++++ .../e2e/pages/view-organization-page.ts | 13 ++++++++ ...create-and-remove-role-assignments.spec.ts | 33 +++++++++++++++++++ .../rbac-offcanvas.component.html | 3 +- .../rbac-offcanvas.component.ts | 4 ++- .../rbac-widget/rbac-widget.component.html | 1 + .../rbac-widget/rbac-widget.component.ts | 3 +- .../create-api-key.component.html | 8 ++++- .../create-api-key.component.ts | 4 ++- .../view-organization.component.html | 1 + 11 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts create mode 100644 src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts diff --git a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts index b4fbdd34..45cfa5d2 100644 --- a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts +++ b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts @@ -26,15 +26,29 @@ export const turnierplan = { navigationTab: (id: number) => `page-frame-navigation-tab-${id}`, title: 'page-frame-title' }, + rbacWidget: { + openOffcanvasButton: 'rbac-widget-open-offcanvas-button' + }, + rbacOffcanvas: { + doneButton: 'rbac-offcanvas-done-button', + assignmentsCount: 'rbac-offcanvas-assignments-count' + }, viewOrganizationPage: { tournamentsPageId: 0, + apiKeysPageId: 2, settingsPageId: 3, - newTournamentButton: 'view-organization-page-new-tournament-button' + newTournamentButton: 'view-organization-page-new-tournament-button', + newApiKeyButton: 'view-organization-page-new-api-key-button' }, createTournamentPage: { confirmButton: 'create-tournament-page-confirm-button', tournamentNameField: 'create-tournament-page-tournament-name-field' }, + createApiKeyPage: { + apiKeyNameField: 'create-api-key-page-api-key-name-field', + confirmButton: 'create-api-key-page-confirm-button', + doneButton: 'create-api-key-page-done-button' + }, configureTournamentPage: { addGroupButton: 'configure-tournament-page-add-group-button', shuffleGroupsButton: 'configure-tournament-page-shuffle-groups-button', diff --git a/src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts b/src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts new file mode 100644 index 00000000..4cc997b0 --- /dev/null +++ b/src/Turnierplan.App/Client/e2e/pages/rbac-offcanvas.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; +import { turnierplan } from '../consts/turnierplan'; + +export class RbacOffcanvas { + constructor(private readonly page: Page) {} + + public getRoleAssignmentsCountLocator(): Locator { + return this.page.getByTestId(turnierplan.rbacOffcanvas.assignmentsCount); + } + + public async close(): Promise { + await this.page.getByTestId(turnierplan.rbacOffcanvas.doneButton).click(); + } +} diff --git a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts index 73ba5271..3c3e65cf 100644 --- a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts +++ b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts @@ -11,10 +11,23 @@ export class ViewOrganizationPage { await this.page.getByTestId(turnierplan.createTournamentPage.confirmButton).click(); } + public async createApiKey(name: string): Promise { + await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.apiKeysPageId)).click(); + await this.page.getByTestId(turnierplan.viewOrganizationPage.newApiKeyButton).click(); + await this.page.getByTestId(turnierplan.createApiKeyPage.apiKeyNameField).fill(name); + await this.page.getByTestId(turnierplan.createApiKeyPage.confirmButton).click(); + await this.page.getByTestId(turnierplan.createApiKeyPage.doneButton).click(); + } + public async deleteOrganization(confirmText: string): Promise { await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.settingsPageId)).click(); await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(confirmText); await this.page.getByTestId(turnierplan.deleteWidget.deleteButton).click(); await this.page.getByTestId(turnierplan.deleteModal.confirmDeleteButton).click(); } + + public async openRoleAssignments(): Promise { + await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.settingsPageId)).click(); + await this.page.getByTestId(turnierplan.rbacWidget.openOffcanvasButton).click(); + } } diff --git a/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts b/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts new file mode 100644 index 00000000..a31db7b2 --- /dev/null +++ b/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test'; +import { createIdentifier } from '../utils/create-identifier'; +import { LoginPage } from '../pages/login-page'; +import { LandingPage } from '../pages/landing-page'; +import { ViewOrganizationPage } from '../pages/view-organization-page'; +import { RbacOffcanvas } from '../pages/rbac-offcanvas'; + +test('Create API key, then create and remove role assignment', async ({ page }) => { + const organizationName = createIdentifier(); + + await new LoginPage(page).login(); + await new LandingPage(page).createOrganization(organizationName); + + const organizationPage = new ViewOrganizationPage(page); + const rbacOffcanvas = new RbacOffcanvas(page); + + await organizationPage.openRoleAssignments(); + await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('1 Zuweisung'); + await rbacOffcanvas.close(); + + const apiKeyName = createIdentifier(); + await organizationPage.createApiKey(apiKeyName); + + await organizationPage.openRoleAssignments(); + await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('2 Zuweisungen'); + await rbacOffcanvas.close(); + + // TODO: Delete API key + + await organizationPage.openRoleAssignments(); + await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('1 Zuweisung'); + await rbacOffcanvas.close(); +}); diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html index c40ec0f3..2bc2ee10 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html @@ -1,6 +1,7 @@
-
+
@if (roleAssignmentCount === 1) { } @else { diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts index 98cf18cd..d4f96e7f 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.ts @@ -14,6 +14,7 @@ import { RoleAssignmentDto } from '../../../api/models/role-assignment-dto'; import { TurnierplanApi } from '../../../api/turnierplan-api'; import { deleteRoleAssignment } from '../../../api/fn/role-assignments/delete-role-assignment'; import { getRoleAssignments } from '../../../api/fn/role-assignments/get-role-assignments'; +import { E2eDirective } from '../../../core/directives/e2e.directive'; interface IRbacOffcanvasTarget { name: string; @@ -30,7 +31,8 @@ interface IRbacOffcanvasTarget { RbacPrincipalComponent, DeleteButtonComponent, TranslatePipe, - TranslateDatePipe + TranslateDatePipe, + E2eDirective ] }) export class RbacOffcanvasComponent implements OnDestroy { diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html index 923187b0..ff6003b5 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-widget/rbac-widget.component.html @@ -10,6 +10,7 @@
- +
} @else { @@ -38,6 +42,7 @@ type="text" class="form-control" formControlName="name" + [tpE2E]="'create-api-key-page-api-key-name-field'" [ngClass]="nameControl.dirty || nameControl.touched ? (nameControl.invalid ? 'is-invalid' : 'is-valid') : ''" />
@@ -90,6 +95,7 @@
Date: Sun, 22 Feb 2026 09:55:11 +0100 Subject: [PATCH 08/11] Finish e2e --- .../Client/e2e/consts/turnierplan.ts | 5 ++++- .../e2e/pages/view-organization-page.ts | 21 ++++++++++++++++--- ...create-and-remove-role-assignments.spec.ts | 4 ++-- .../create-api-key.component.html | 16 ++++++++++++-- .../view-organization.component.html | 1 + 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts index 45cfa5d2..06af01e2 100644 --- a/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts +++ b/src/Turnierplan.App/Client/e2e/consts/turnierplan.ts @@ -38,7 +38,8 @@ export const turnierplan = { apiKeysPageId: 2, settingsPageId: 3, newTournamentButton: 'view-organization-page-new-tournament-button', - newApiKeyButton: 'view-organization-page-new-api-key-button' + newApiKeyButton: 'view-organization-page-new-api-key-button', + deleteApiKeyButton: (id: string) => `view-organization-page-delete-api-key-button-${id}` }, createTournamentPage: { confirmButton: 'create-tournament-page-confirm-button', @@ -47,6 +48,8 @@ export const turnierplan = { createApiKeyPage: { apiKeyNameField: 'create-api-key-page-api-key-name-field', confirmButton: 'create-api-key-page-confirm-button', + resultIdField: 'create-api-key-page-result-id-field', + resultSecretField: 'create-api-key-page-result-secret-field', doneButton: 'create-api-key-page-done-button' }, configureTournamentPage: { diff --git a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts index 3c3e65cf..552f2347 100644 --- a/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts +++ b/src/Turnierplan.App/Client/e2e/pages/view-organization-page.ts @@ -11,17 +11,32 @@ export class ViewOrganizationPage { await this.page.getByTestId(turnierplan.createTournamentPage.confirmButton).click(); } - public async createApiKey(name: string): Promise { + public async createApiKey(name: string): Promise<{ id: string; secret: string }> { await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.apiKeysPageId)).click(); await this.page.getByTestId(turnierplan.viewOrganizationPage.newApiKeyButton).click(); await this.page.getByTestId(turnierplan.createApiKeyPage.apiKeyNameField).fill(name); await this.page.getByTestId(turnierplan.createApiKeyPage.confirmButton).click(); + + await this.page.getByTestId(turnierplan.createApiKeyPage.resultIdField).waitFor({ state: 'visible' }); + const resultId = await this.page.getByTestId(turnierplan.createApiKeyPage.resultIdField).inputValue(); + const resultSecret = await this.page.getByTestId(turnierplan.createApiKeyPage.resultSecretField).inputValue(); + await this.page.getByTestId(turnierplan.createApiKeyPage.doneButton).click(); + + return { id: resultId, secret: resultSecret }; + } + + public async deleteApiKeyWithId(id: string, name: string): Promise { + await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.apiKeysPageId)).click(); + await this.page.getByTestId(turnierplan.viewOrganizationPage.deleteApiKeyButton(id)).click(); + await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(name); + await this.page.getByTestId(turnierplan.deleteWidget.deleteButton).click(); + await this.page.getByTestId(turnierplan.deleteModal.confirmDeleteButton).click(); } - public async deleteOrganization(confirmText: string): Promise { + public async deleteOrganization(name: string): Promise { await this.page.getByTestId(turnierplan.pageFrame.navigationTab(turnierplan.viewOrganizationPage.settingsPageId)).click(); - await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(confirmText); + await this.page.getByTestId(turnierplan.deleteWidget.confirmationField).fill(name); await this.page.getByTestId(turnierplan.deleteWidget.deleteButton).click(); await this.page.getByTestId(turnierplan.deleteModal.confirmDeleteButton).click(); } diff --git a/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts b/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts index a31db7b2..ae4e099a 100644 --- a/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts +++ b/src/Turnierplan.App/Client/e2e/tests/create-and-remove-role-assignments.spec.ts @@ -19,13 +19,13 @@ test('Create API key, then create and remove role assignment', async ({ page }) await rbacOffcanvas.close(); const apiKeyName = createIdentifier(); - await organizationPage.createApiKey(apiKeyName); + const apiKeyDetails = await organizationPage.createApiKey(apiKeyName); await organizationPage.openRoleAssignments(); await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('2 Zuweisungen'); await rbacOffcanvas.close(); - // TODO: Delete API key + await organizationPage.deleteApiKeyWithId(apiKeyDetails.id, apiKeyName); await organizationPage.openRoleAssignments(); await expect(rbacOffcanvas.getRoleAssignmentsCountLocator()).toHaveText('1 Zuweisung'); diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/create-api-key/create-api-key.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/create-api-key/create-api-key.component.html index 406031cd..e865da26 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/create-api-key/create-api-key.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/create-api-key/create-api-key.component.html @@ -5,7 +5,13 @@
- + @@ -15,7 +21,13 @@
- + diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html index 99ed0fd3..cbbe86bd 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-organization/view-organization.component.html @@ -219,6 +219,7 @@ @if (writeAllowed) {
- +
Date: Sun, 22 Feb 2026 18:39:18 +0100 Subject: [PATCH 09/11] Move class --- .../rbac-offcanvas/rbac-offcanvas.component.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html index 2bc2ee10..935f066c 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html +++ b/src/Turnierplan.App/Client/src/app/portal/components/rbac-offcanvas/rbac-offcanvas.component.html @@ -27,14 +27,11 @@ (buttonClick)="showAddRoleAssignmentDialog()" /> -
+
@if (roleAssignmentCount === 1) { - + } @else { - + }
From 1efa09dd06a47c74960e338b1e187307ef593b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 19:34:42 +0100 Subject: [PATCH 10/11] Add functional test --- src/Turnierplan.App.Test.Functional/Routes.cs | 21 +++++ .../Scenarios.cs | 58 ++++++++++++++ .../TestServer.cs | 79 +++++++++++++++++++ .../Turnierplan.App.Test.Functional.csproj | 19 +++++ src/turnierplan.NET.slnx | 1 + 5 files changed, 178 insertions(+) create mode 100644 src/Turnierplan.App.Test.Functional/Routes.cs create mode 100644 src/Turnierplan.App.Test.Functional/Scenarios.cs create mode 100644 src/Turnierplan.App.Test.Functional/TestServer.cs create mode 100644 src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj diff --git a/src/Turnierplan.App.Test.Functional/Routes.cs b/src/Turnierplan.App.Test.Functional/Routes.cs new file mode 100644 index 00000000..9de37e9e --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/Routes.cs @@ -0,0 +1,21 @@ +using Turnierplan.Core.PublicId; + +namespace Turnierplan.App.Test.Functional; + +internal static class Routes +{ + public static class ApiKeys + { + public static string Delete(PublicId id) => $"/api/api-keys/{id}"; + } + + public static class Identity + { + public static string Login() => "/api/identity/login"; + } + + public static class Users + { + public static string Delete(Guid id) => $"/api/users/{id}"; + } +} diff --git a/src/Turnierplan.App.Test.Functional/Scenarios.cs b/src/Turnierplan.App.Test.Functional/Scenarios.cs new file mode 100644 index 00000000..a8c1ea09 --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/Scenarios.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using FluentAssertions.Extensions; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Extensions; +using Turnierplan.Core.Organization; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.User; +using Xunit; + +namespace Turnierplan.App.Test.Functional; + +public sealed class Scenarios +{ + private readonly TestServer _testServer = new(); + + [Fact] + public async Task When_ApiKey_And_User_Are_Deleted_The_Role_Assignments_Are_Also_Removed() + { + var (apiKeyId, userId) = _testServer.ExecuteContextAction(db => + { + var user = new User(string.Empty); + db.Users.Add(user); + + var org = new Organization(string.Empty); + db.Organizations.Add(org); + + var key = new ApiKey(org, string.Empty, null, DateTime.UtcNow + 1.Days()); + db.ApiKeys.Add(key); + + var tournament = new Tournament(org, string.Empty, Visibility.Public); + db.Tournaments.Add(tournament); + + org.AddRoleAssignment(Role.Reader, user.AsPrincipal()); + tournament.AddRoleAssignment(Role.Contributor, user.AsPrincipal()); + tournament.AddRoleAssignment(Role.Reader, key.AsPrincipal()); + + db.SaveChanges(); + + return (key.PublicId, user.Id); + }); + + _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1); + _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(2); + + var resp = await _testServer.Client.DeleteAsync(Routes.ApiKeys.Delete(apiKeyId), TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + + _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1); + _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(1); + + resp = await _testServer.Client.DeleteAsync(Routes.Users.Delete(userId), TestContext.Current.CancellationToken); + resp.EnsureSuccessStatusCode(); + + _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(0); + _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(0); + } +} diff --git a/src/Turnierplan.App.Test.Functional/TestServer.cs b/src/Turnierplan.App.Test.Functional/TestServer.cs new file mode 100644 index 00000000..b0d1e9f7 --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/TestServer.cs @@ -0,0 +1,79 @@ +using System.Net.Http.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Turnierplan.Core.User; +using Turnierplan.Dal; + +namespace Turnierplan.App.Test.Functional; + +internal sealed class TestServer +{ + private readonly WebApplicationFactory _application; + + public TestServer() + { + _application = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection([ + new KeyValuePair("Database:InMemory", "true"), + new KeyValuePair("Identity:UseInsecureCookies", "true") + ]); + }); + }); + + const string username = "functional_test"; + const string password = "P@ssw0rd"; + + using (var scope = _application.Services.CreateScope()) + { + var ctx = scope.ServiceProvider.GetRequiredService(); + + var user = new User(username) + { + IsAdministrator = true + }; + + user.UpdatePassword(scope.ServiceProvider.GetRequiredService>().HashPassword(user, password)); + + ctx.Users.Add(user); + ctx.SaveChanges(); + } + + var loginRequest = new HttpRequestMessage(HttpMethod.Post, Routes.Identity.Login()) + { + Content = JsonContent.Create(new { UserName = username, Password = password}) + }; + + Client = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true }); + var loginResponseTask = Client.SendAsync(loginRequest); + loginResponseTask.Wait(); + var loginResponse = loginResponseTask.Result; + loginResponse.EnsureSuccessStatusCode(); + } + + public HttpClient Client { get; } + + public void ExecuteContextAction(Action action) + { + using var scope = _application.Services.CreateScope(); + action(scope.ServiceProvider.GetRequiredService()); + } + + public T ExecuteContextAction(Func action) + { + using var scope = _application.Services.CreateScope(); + return action(scope.ServiceProvider.GetRequiredService()); + } + + private sealed record LoginResult + { + public required bool Success { get; init; } + + public string? AccessToken { get; init; } + } +} diff --git a/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj b/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj new file mode 100644 index 00000000..fe0ea8e1 --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj @@ -0,0 +1,19 @@ + + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/src/turnierplan.NET.slnx b/src/turnierplan.NET.slnx index 03a88477..b163cf28 100644 --- a/src/turnierplan.NET.slnx +++ b/src/turnierplan.NET.slnx @@ -5,6 +5,7 @@ + From cf9765dec8e555c8bc2cb17d1737ca7e328df8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 22 Feb 2026 19:37:09 +0100 Subject: [PATCH 11/11] Delete unused method --- src/Turnierplan.App.Test.Functional/TestServer.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Turnierplan.App.Test.Functional/TestServer.cs b/src/Turnierplan.App.Test.Functional/TestServer.cs index b0d1e9f7..92cb0ca0 100644 --- a/src/Turnierplan.App.Test.Functional/TestServer.cs +++ b/src/Turnierplan.App.Test.Functional/TestServer.cs @@ -69,11 +69,4 @@ public T ExecuteContextAction(Func action) using var scope = _application.Services.CreateScope(); return action(scope.ServiceProvider.GetRequiredService()); } - - private sealed record LoginResult - { - public required bool Success { get; init; } - - public string? AccessToken { get; init; } - } }