From 28a36ac78a2d493f11ed74d87f9c84aa31f0a694 Mon Sep 17 00:00:00 2001 From: Vladimir Vuckovic <37671301+stamenione@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:13:55 +0200 Subject: [PATCH] AdminUI: Display of the Identity deletion audit log for deleted identities (#684) * feat: add components template * feat: add template for endpoint * feat: add query to retrieve audit logs of identity * test: add unit tests for new audti log query * feat: add endpoint for identity deletion process audit logs * test: update sdk and integration tests * test: refactor and add test for multiple deletion processes * feat: add deletion process audit logs for an identity page * chore: run prettier * fix: change the type * chore: make the table present correct data * fix: audit logs of deleted identities should be returned * fix: should not check for identity existence since behavior should work for deleted identities * refactor: remove unnecessary produces error attribute * chore: add deletion process cancellation to admin api sdk * refactor: hash identity outside repository * test: create test data generator method to create cancelled deletion process * test: add deletion processes in different statuses to test * refactor: order audit log entries by created at * fix: the header title and remove expansion panel * fix: formatting * fix: minimize the top margin * fix: remove uneccasary sorting * test: refactor unit tests to use fake dbcontext * test: simplify Handler tests * refactor: change order of CreateDbContexts return values * refactor: extract common code into private method * refactor: add IEventBus parameter to DevicesDbContext --------- Co-authored-by: Daniel Almeida <115644988+daniel-almeida-konkconsulting@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Timo Notheisen --- .../Identities/IdentitiesEndpoint.cs | 10 ++ ...IdentityDeletionProcessAuditLogEntryDTO.cs | 10 ++ .../CancelDeletionProcessAsSupport.cs | 8 ++ ...dentityDeletionProcessAuditLogsResponse.cs | 5 + .../ClientApp/src/app/app-routing.module.ts | 2 + .../AdminApi/ClientApp/src/app/app.module.ts | 6 +- .../dp-audit-logs-details.component.css | 133 ++++++++++++++++++ .../dp-audit-logs-details.component.html | 70 +++++++++ .../dp-audit-logs-details.component.spec.ts | 22 +++ .../dp-audit-logs-details.component.ts | 102 ++++++++++++++ .../dp-audit-logs/dp-audit-logs.component.css | 27 ++++ .../dp-audit-logs.component.html | 15 ++ .../dp-audit-logs.component.spec.ts | 22 +++ .../dp-audit-logs/dp-audit-logs.component.ts | 33 +++++ .../identity-list.component.html | 6 + .../identity-service/identity.service.ts | 4 + .../Controllers/IdentitiesController.cs | 9 ++ .../AuditLogs/GET.feature | 11 ++ .../IdentitiesApiStepDefinitions.cs | 19 +++ .../TestDoubles/Fakes/FakeDbContextFactory.cs | 5 +- .../DTOs/IdentityDeletionProcessDetailsDTO.cs | 1 + .../GetDeletionProcessesAuditLogsQuery.cs | 13 ++ .../GetDeletionProcessesAuditLogsResponse.cs | 13 ++ .../GetDeletionProcessesAuditLogs/Handler.cs | 21 +++ .../Repository/IIdentitiesRepository.cs | 4 + .../Persistence/Database/DevicesDbContext.cs | 23 ++- .../Repository/IdentitiesRepository.cs | 10 +- .../ApplicationDbContextExtensions.cs | 6 + .../TestDataGenerator.cs | 36 +++++ ...ProcessAuditLogsByAddressStubRepository.cs | 84 +++++++++++ .../HandlerTests.cs | 128 +++++++++++++++++ .../FindByAddressStubRepository.cs | 13 +- .../ListIdentities/FindAllStubRepository.cs | 11 +- .../Repositories/MessagesRepositoryTests.cs | 2 +- ...ionshipTemplateQueryableExtensionsTests.cs | 2 +- .../HandlerTests.cs | 2 +- 36 files changed, 857 insertions(+), 31 deletions(-) create mode 100644 AdminApi.Sdk/Endpoints/Identities/Types/IdentityDeletionProcessAuditLogEntryDTO.cs create mode 100644 AdminApi.Sdk/Endpoints/Identities/Types/Responses/CancelDeletionProcessAsSupport.cs create mode 100644 AdminApi.Sdk/Endpoints/Identities/Types/Responses/ListIdentityDeletionProcessAuditLogsResponse.cs create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.css create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.html create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.spec.ts create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.ts create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.css create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.html create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.spec.ts create mode 100644 AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.ts create mode 100644 AdminApi/test/AdminApi.Tests.Integration/Features/IdentityDeletionProcess/AuditLogs/GET.feature create mode 100644 Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsQuery.cs create mode 100644 Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsResponse.cs create mode 100644 Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs create mode 100644 Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/FindDeletionProcessAuditLogsByAddressStubRepository.cs create mode 100644 Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/HandlerTests.cs diff --git a/AdminApi.Sdk/Endpoints/Identities/IdentitiesEndpoint.cs b/AdminApi.Sdk/Endpoints/Identities/IdentitiesEndpoint.cs index e271d8e4b1..b68ab4d544 100644 --- a/AdminApi.Sdk/Endpoints/Identities/IdentitiesEndpoint.cs +++ b/AdminApi.Sdk/Endpoints/Identities/IdentitiesEndpoint.cs @@ -25,6 +25,11 @@ public async Task> ListIdentities() .ExecuteOData(); } + public async Task?> ListIdentityDeletionProcessAuditLogs(string address) + { + return await _client.Get($"api/{API_VERSION}/Identities/{address}/DeletionProcesses/AuditLogs"); + } + public async Task> GetIdentity(string address) { return await _client.Get($"api/{API_VERSION}/Identities/{address}"); @@ -44,4 +49,9 @@ public async Task> StartDelet { return await _client.Post($"api/{API_VERSION}/Identities/{address}/DeletionProcesses"); } + + public async Task> CancelDeletionProcess(string address, string deletionProcessId) + { + return await _client.Put($"api/{API_VERSION}/Identities/{address}/DeletionProcesses/{deletionProcessId}/Cancel"); + } } diff --git a/AdminApi.Sdk/Endpoints/Identities/Types/IdentityDeletionProcessAuditLogEntryDTO.cs b/AdminApi.Sdk/Endpoints/Identities/Types/IdentityDeletionProcessAuditLogEntryDTO.cs new file mode 100644 index 0000000000..581398a193 --- /dev/null +++ b/AdminApi.Sdk/Endpoints/Identities/Types/IdentityDeletionProcessAuditLogEntryDTO.cs @@ -0,0 +1,10 @@ +namespace Backbone.AdminApi.Sdk.Endpoints.Identities.Types; + +public class IdentityDeletionProcessAuditLogEntryDTO +{ + public required string Id { get; set; } + public required DateTime CreatedAt { get; set; } + public required string MessageKey { get; set; } + public required string? OldStatus { get; set; } + public required string NewStatus { get; set; } +} diff --git a/AdminApi.Sdk/Endpoints/Identities/Types/Responses/CancelDeletionProcessAsSupport.cs b/AdminApi.Sdk/Endpoints/Identities/Types/Responses/CancelDeletionProcessAsSupport.cs new file mode 100644 index 0000000000..935d0c7757 --- /dev/null +++ b/AdminApi.Sdk/Endpoints/Identities/Types/Responses/CancelDeletionProcessAsSupport.cs @@ -0,0 +1,8 @@ +namespace Backbone.AdminApi.Sdk.Endpoints.Identities.Types.Responses; + +public class CancelDeletionAsSupportResponse +{ + public required string Id { get; set; } + public required string Status { get; set; } + public required DateTime CancelledAt { get; set; } +} diff --git a/AdminApi.Sdk/Endpoints/Identities/Types/Responses/ListIdentityDeletionProcessAuditLogsResponse.cs b/AdminApi.Sdk/Endpoints/Identities/Types/Responses/ListIdentityDeletionProcessAuditLogsResponse.cs new file mode 100644 index 0000000000..a3c76e10d8 --- /dev/null +++ b/AdminApi.Sdk/Endpoints/Identities/Types/Responses/ListIdentityDeletionProcessAuditLogsResponse.cs @@ -0,0 +1,5 @@ +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; + +namespace Backbone.AdminApi.Sdk.Endpoints.Identities.Types.Responses; + +public class ListIdentityDeletionProcessAuditLogsResponse : EnumerableResponseBase; diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/app-routing.module.ts b/AdminApi/src/AdminApi/ClientApp/src/app/app-routing.module.ts index eff9aed022..bb20a48c36 100644 --- a/AdminApi/src/AdminApi/ClientApp/src/app/app-routing.module.ts +++ b/AdminApi/src/AdminApi/ClientApp/src/app/app-routing.module.ts @@ -12,6 +12,7 @@ import { TierListComponent } from "./components/quotas/tier/tier-list/tier-list. import { LoginComponent } from "./components/shared/login/login.component"; import { AuthGuard } from "./shared/auth-guard/auth-guard.guard"; import { CustomRouteReuseStrategy } from "./utils/custom-route-reuse-strategy"; +import { DeletionProcessAuditLogsDetailsComponent } from "./components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component"; const routes: Routes = [ { path: "", redirectTo: "/dashboard", pathMatch: "full" }, @@ -19,6 +20,7 @@ const routes: Routes = [ { path: "dashboard", component: DashboardComponent, data: { breadcrumb: "Dashboard" }, canActivate: [AuthGuard] }, { path: "identities", component: IdentityListComponent, data: { breadcrumb: "Identities" }, canActivate: [AuthGuard] }, { path: "identities/:address", component: IdentityDetailsComponent, canActivate: [AuthGuard] }, + { path: "identities/:address/deletion-processes/audit-logs", component: DeletionProcessAuditLogsDetailsComponent, canActivate: [AuthGuard] }, { path: "tiers", component: TierListComponent, data: { breadcrumb: "Tiers" }, canActivate: [AuthGuard] }, { path: "tiers/create", component: TierEditComponent, data: { breadcrumb: "Create Tier" }, canActivate: [AuthGuard] }, { path: "tiers/:id", component: TierEditComponent, canActivate: [AuthGuard] }, diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/app.module.ts b/AdminApi/src/AdminApi/ClientApp/src/app/app.module.ts index 91eb162ff3..0a2c1e393b 100644 --- a/AdminApi/src/AdminApi/ClientApp/src/app/app.module.ts +++ b/AdminApi/src/AdminApi/ClientApp/src/app/app.module.ts @@ -43,6 +43,8 @@ import { CreateClientDialogComponent } from "./components/client/create-client-d import { DashboardComponent } from "./components/dashboard/dashboard.component"; import { PageNotFoundComponent } from "./components/error/page-not-found/page-not-found.component"; import { DeletionProcessesComponent } from "./components/identity/identity-details/deletion-processes/deletion-processes.component"; +import { DeletionProcessAuditLogsDetailsComponent } from "./components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component"; +import { DeletionProcessAuditLogsComponent } from "./components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component"; import { CancelDeletionProcessDialogComponent } from "./components/identity/identity-details/deletion-processes/dp-details/cancel-dp-dialog/cancel-dp-dialog.component"; import { DeletionProcessDetailsComponent } from "./components/identity/identity-details/deletion-processes/dp-details/dp-details.component"; import { StartDeletionProcessDialogComponent } from "./components/identity/identity-details/deletion-processes/start-deletion-process-dialog/start-deletion-process-dialog.component"; @@ -99,7 +101,9 @@ import { XSRFInterceptor } from "./shared/interceptors/xsrf.interceptor"; DeletionProcessesComponent, DeletionProcessDetailsComponent, CancelDeletionProcessDialogComponent, - StartDeletionProcessDialogComponent + StartDeletionProcessDialogComponent, + DeletionProcessAuditLogsComponent, + DeletionProcessAuditLogsDetailsComponent ], imports: [ FormsModule, diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.css b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.css new file mode 100644 index 0000000000..89718c3396 --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.css @@ -0,0 +1,133 @@ +.container { + display: flex; + justify-content: space-between; +} + +.column { + flex: 1; +} + +.action-buttons { + margin-top: 20px; + display: flex; + justify-content: flex-end; +} + +.header-description { + color: #fff; +} + +.action-buttons button { + margin: 10px; +} + +.form-card { + margin-bottom: 20px; +} + +.loading { + text-align: center; +} + +.form-details { + padding: 20px; +} + +.mat-list-item { + display: flex; + justify-content: space-between; +} + +.form-card { + margin-top: 30px; + min-height: 150px; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + z-index: 999; + position: absolute; + margin-top: -60px; + width: 100%; + height: 100%; +} + +.card-header { + border-radius: 3px; + padding: 15px; + background-color: #17428d; + position: relative; + z-index: 1; + margin: 15px 15px -50px 15px; +} + +.header-title { + color: #fff; +} + +.identity-accordion { + margin: 0px 0px 0px 5px !important; +} + +.details-expansion-panel-header { + background: #17428d !important; +} + +.details-expansion-panel-header:hover { + background: #11337a !important; +} + +.details-expansion-panel-header.mat-expansion-panel-header.mat-expanded { + border-radius: 4px 4px 0px 0px; +} + +.details-panel-header-title { + color: white !important; +} + +.details-panel-header-desc { + color: rgba(255, 255, 255, 0.54) !important; +} + +:host ::ng-deep .details-expansion-panel-header > .mat-expansion-indicator::after { + color: white !important; +} + +@media screen and (max-width: 960px) { + .mat-mdc-table .mat-mdc-header-row { + display: none; + } + + .mat-mdc-table .mat-mdc-row { + display: flex; + flex-wrap: wrap; + height: auto; + border-bottom: 1px solid #ddd; + } + + .mat-mdc-table .mat-mdc-cell { + width: 100%; + border-bottom: 0px solid #ddd; + font-size: 1em; + min-height: 30px; + margin-bottom: 4%; + word-break: break-all; + white-space: pre-wrap; + } + + .mat-mdc-table .mat-mdc-cell:before { + content: attr(data-label); + float: left; + font-weight: 500; + } + + .mat-mdc-table .mat-mdc-cell:first-child { + margin-top: 25px; + } + + .mat-mdc-table .mat-mdc-row:last-child { + border-bottom: 0px; + } +} diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.html b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.html new file mode 100644 index 0000000000..f37df2dfcc --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.html @@ -0,0 +1,70 @@ +
+

{{ headerDeletionProcessAuditLog }}

+

{{ headerDeletionProcessAuditLogDescription }}

+
+
+ + +
+ +
+
+ +
+
+ + Identity Address + {{ identityAddress }} + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID + {{ AuditLog.id }} + Created At + {{ AuditLog.createdAt | date }} + Message + {{ replaceMessageKeyWithCorrespondingText(AuditLog.messageKey) }} + Old Status + {{ styleStatus(AuditLog.oldStatus) }} + New Status + {{ styleStatus(AuditLog.newStatus) }} + Identity Deletion Process Id + {{ AuditLog.identityDeletionProcessId }} +
No deletion process audit logs found.
+
+
+
+
diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.spec.ts b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.spec.ts new file mode 100644 index 0000000000..aa6b5586ee --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { DeletionProcessAuditLogsDetailsComponent } from "./dp-audit-logs-details.component"; + +describe("DeletionProcessAuditLogsDetailsComponent", () => { + let component: DeletionProcessAuditLogsDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeletionProcessAuditLogsDetailsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(DeletionProcessAuditLogsDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", async () => { + await expect(component).toBeTruthy(); + }); +}); diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.ts b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.ts new file mode 100644 index 0000000000..60acca5ed4 --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs-details/dp-audit-logs-details/dp-audit-logs-details.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { ActivatedRoute } from "@angular/router"; +import { DeletionProcessAuditLog, IdentityService } from "src/app/services/identity-service/identity.service"; +import { HttpResponseEnvelope } from "src/app/utils/http-response-envelope"; + +@Component({ + selector: "app-dp-audit-logs-details", + templateUrl: "./dp-audit-logs-details.component.html", + styleUrls: ["./dp-audit-logs-details.component.css"] +}) +export class DeletionProcessAuditLogsDetailsComponent implements OnInit { + public identityDeletionProcessID: string; + public identityAddress: string; + + public header: string; + public headerDeletionProcessAuditLog: string; + public headerDeletionProcessAuditLogDescription: string; + + public loading: boolean; + public deletionProcessesAuditLogTableDisplayedColumns: string[]; + + public identityDeletionProcessAuditLogs: DeletionProcessAuditLog[] = []; + + public constructor( + private readonly identityService: IdentityService, + private readonly snackBar: MatSnackBar, + private readonly activatedRoute: ActivatedRoute + ) { + this.header = "Deletion Process Details"; + this.headerDeletionProcessAuditLog = "Deletion Process Audit Logs"; + this.headerDeletionProcessAuditLogDescription = "View deletion process audit logs for Identity."; + this.loading = false; + this.deletionProcessesAuditLogTableDisplayedColumns = ["id", "createdAt", "message", "oldStatus", "newStatus"]; + + this.identityDeletionProcessID = ""; + this.identityAddress = ""; + } + + public ngOnInit(): void { + this.activatedRoute.params.subscribe((params) => { + this.identityAddress = params["address"]; + this.loadIdentityDeletionProcessAuditLogs(); + }); + } + + private loadIdentityDeletionProcessAuditLogs(): void { + this.loading = true; + this.identityService.getDeletionProcessAuditLogsOfIdentity(this.identityAddress.trim()).subscribe({ + next: (data: HttpResponseEnvelope) => { + this.identityDeletionProcessAuditLogs = data.result; + this.loading = false; + }, + error: (err: any) => { + const errorMessage = err.error?.error?.message ?? err.message; + this.snackBar.open(errorMessage, "Dismiss", { + verticalPosition: "top", + horizontalPosition: "center" + }); + this.loading = false; + } + }); + } + + public styleStatus(status: string): string { + if (status === "WaitingForApproval") return "Waiting for Approval"; + return status; + } + + public replaceMessageKeyWithCorrespondingText(messageKey: string): string { + switch (messageKey) { + case "StartedByOwner": + return "The deletion process was started by the owner. It was automatically approved."; + case "StartedBySupport": + return "The deletion process was started by support. It is now waiting for approval."; + case "Approved": + return "The deletion process was approved."; + case "Rejected": + return "The deletion process was rejected."; + case "CancelledByOwner": + return "The deletion process was cancelled by the owner of the identity."; + case "CancelledBySupport": + return "The deletion process was cancelled by a support employee."; + case "CancelledAutomatically": + return "The deletion process was cancelled automatically, because it wasn't approved by the owner within the approval period."; + case "ApprovalReminder1Sent": + return "The first approval reminder notification has been sent."; + case "ApprovalReminder2Sent": + return "The second approval reminder notification has been sent."; + case "ApprovalReminder3Sent": + return "The third approval reminder notification has been sent."; + case "GracePeriodReminder1Sent": + return "The first grace period reminder notification has been sent."; + case "GracePeriodReminder2Sent": + return "The second grace period reminder notification has been sent."; + case "GracePeriodReminder3Sent": + return "The third grace period reminder notification has been sent."; + default: + return "Unknown message key."; + } + } +} diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.css b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.css new file mode 100644 index 0000000000..edd47ec8c0 --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.css @@ -0,0 +1,27 @@ +.form-container { + display: flex; + align-items: center; +} + +.form-field { + flex: 0 0 25%; + min-width: 25%; +} + +.find-button { + height: 56px; + margin-left: 8px; + margin-bottom: 21px; +} + +.mat-form-field { + width: 100%; +} + +.mat-form-field-infix { + height: 56px; +} + +.custom-mat-hint { + font-size: 16px; +} diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.html b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.html new file mode 100644 index 0000000000..c78ee915d9 --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.html @@ -0,0 +1,15 @@ +
+
+

{{ headerTitle }}

+
+ +
+ + Address + + Enter an identity address + + +
+
+
diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.spec.ts b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.spec.ts new file mode 100644 index 0000000000..354cce0aca --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { DeletionProcessAuditLogsComponent } from "./dp-audit-logs.component"; + +describe("DeletionProcessAuditLogsComponent", () => { + let component: DeletionProcessAuditLogsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DeletionProcessAuditLogsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(DeletionProcessAuditLogsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", async () => { + await expect(component).toBeTruthy(); + }); +}); diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.ts b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.ts new file mode 100644 index 0000000000..7777e8cf0d --- /dev/null +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-details/deletion-processes/dp-audit-logs/dp-audit-logs.component.ts @@ -0,0 +1,33 @@ +import { Component } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Router } from "@angular/router"; +import { IdentityService } from "src/app/services/identity-service/identity.service"; +@Component({ + selector: "app-dp-audit-logs", + templateUrl: "./dp-audit-logs.component.html", + styleUrls: ["./dp-audit-logs.component.css"] +}) +export class DeletionProcessAuditLogsComponent { + public disabled: boolean; + public identityAddress: string; + public headerTitle: string; + + public constructor( + private readonly router: Router, + private readonly snackBar: MatSnackBar, + private readonly identityService: IdentityService + ) { + this.disabled = false; + + this.identityAddress = ""; + this.headerTitle = "Identity deletion process audit logs"; + } + + public async navigateToIdentityDeletionProcessAuditLogs(): Promise { + await this.router.navigate([`identities/${this.identityAddress}/deletion-processes/audit-logs`]); + } + + public isNotEmptyIdentityAddress(): boolean { + return this.identityAddress.trim().length > 0; + } +} diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-list/identity-list.component.html b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-list/identity-list.component.html index 68459799c4..aae817955c 100644 --- a/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-list/identity-list.component.html +++ b/AdminApi/src/AdminApi/ClientApp/src/app/components/identity/identity-list/identity-list.component.html @@ -7,3 +7,9 @@

{{ header }}

+
+ + + + + diff --git a/AdminApi/src/AdminApi/ClientApp/src/app/services/identity-service/identity.service.ts b/AdminApi/src/AdminApi/ClientApp/src/app/services/identity-service/identity.service.ts index fc039a4f3c..87131af94d 100644 --- a/AdminApi/src/AdminApi/ClientApp/src/app/services/identity-service/identity.service.ts +++ b/AdminApi/src/AdminApi/ClientApp/src/app/services/identity-service/identity.service.ts @@ -201,6 +201,10 @@ export class IdentityService { return this.http.get>(`${this.apiUrl}/${address}/DeletionProcesses/${deletionProcessId}`); } + public getDeletionProcessAuditLogsOfIdentity(address: String): Observable> { + return this.http.get>(`${this.apiUrl}/${address}/DeletionProcesses/AuditLogs`); + } + public cancelDeletionProcessAsSupport(identityAddress: string, deletionProcessId: string): Observable> { return this.http.put>(`${this.apiUrl}/${identityAddress}/DeletionProcesses/${deletionProcessId}/Cancel`, ""); } diff --git a/AdminApi/src/AdminApi/Controllers/IdentitiesController.cs b/AdminApi/src/AdminApi/Controllers/IdentitiesController.cs index ea51c504c5..1541ea58ee 100644 --- a/AdminApi/src/AdminApi/Controllers/IdentitiesController.cs +++ b/AdminApi/src/AdminApi/Controllers/IdentitiesController.cs @@ -9,6 +9,7 @@ using Backbone.Modules.Devices.Application.Identities.Commands.UpdateIdentity; using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessAsSupport; using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAsSupport; +using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAuditLogs; using Backbone.Modules.Quotas.Application.DTOs; using Backbone.Modules.Quotas.Application.Identities.Commands.CreateQuotaForIdentity; using Backbone.Modules.Quotas.Application.Identities.Commands.DeleteQuotaForIdentity; @@ -126,6 +127,14 @@ public async Task GetDeletionProcessesAsSupport([FromRoute] strin return Ok(response); } + [HttpGet("{identityAddress}/DeletionProcesses/AuditLogs")] + [ProducesResponseType(typeof(GetDeletionProcessesAuditLogsResponse), StatusCodes.Status200OK)] + public async Task GetDeletionProcessesAuditLogs([FromRoute] string identityAddress, CancellationToken cancellationToken) + { + var response = await _mediator.Send(new GetDeletionProcessesAuditLogsQuery(identityAddress), cancellationToken); + return Ok(response); + } + [HttpPut("{address}/DeletionProcesses/{deletionProcessId}/Cancel")] [ProducesResponseType(typeof(HttpResponseEnvelopeResult), StatusCodes.Status200OK)] [ProducesError(StatusCodes.Status400BadRequest)] diff --git a/AdminApi/test/AdminApi.Tests.Integration/Features/IdentityDeletionProcess/AuditLogs/GET.feature b/AdminApi/test/AdminApi.Tests.Integration/Features/IdentityDeletionProcess/AuditLogs/GET.feature new file mode 100644 index 0000000000..9f0c19ba85 --- /dev/null +++ b/AdminApi/test/AdminApi.Tests.Integration/Features/IdentityDeletionProcess/AuditLogs/GET.feature @@ -0,0 +1,11 @@ +@Integration +Feature: GET Identities/{identityAddress}/DeletionProcesses/AuditLogs + +User requests the Audit Logs pertaining to all Deletion Processes of an Identity + +Scenario: Requesting Audit Logs of Deletion Processes of Identity + Given an Identity i + And an active deletion process for Identity i exists + When a GET request is sent to the /Identities/{i.address}/DeletionProcesses/AuditLogs endpoint + Then the response status code is 200 (OK) + And the response contains a list of Identity Deletion Process Audit Logs diff --git a/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs b/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs index 7c7bc1b96c..f81eb795ab 100644 --- a/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs +++ b/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs @@ -10,12 +10,14 @@ namespace Backbone.AdminApi.Tests.Integration.StepDefinitions; [Binding] [Scope(Feature = "GET Identities")] [Scope(Feature = "POST Identities/{id}/DeletionProcess")] +[Scope(Feature = "GET Identities/{identityAddress}/DeletionProcesses/AuditLogs")] internal class IdentitiesApiStepDefinitions : BaseStepDefinitions { private ApiResponse? _identityOverviewsResponse; private ApiResponse? _identityResponse; private ApiResponse? _createIdentityResponse; private ApiResponse? _identityDeletionProcessResponse; + private ApiResponse? _identityDeletionProcessAuditLogsResponse; private string _existingIdentity; public IdentitiesApiStepDefinitions(HttpClientFactory factory, IOptions options) : base(factory, options) @@ -50,6 +52,12 @@ public async Task WhenAGETRequestIsSentToTheIdentitiesOverviewEndpoint() _identityOverviewsResponse = await _client.Identities.ListIdentities(); } + [When("a GET request is sent to the /Identities/{i.address}/DeletionProcesses/AuditLogs endpoint")] + public async Task WhenAGETRequestIsSentToTheIdentitiesDeletionProcessesAuditLogsEndpoint() + { + _identityDeletionProcessAuditLogsResponse = await _client.Identities.ListIdentityDeletionProcessAuditLogs(_existingIdentity); + } + [When("a GET request is sent to the /Identities/{i.address} endpoint")] public async Task WhenAGETRequestIsSentToTheIdentitiesAddressEndpoint() { @@ -70,6 +78,14 @@ public void ThenTheResponseContainsAListOfIdentities() _identityOverviewsResponse.Should().ComplyWithSchema(); } + [Then("the response contains a list of Identity Deletion Process Audit Logs")] + public void ThenTheResponseContainsAListOfIdentityDeletionProcessAuditLogs() + { + _identityDeletionProcessAuditLogsResponse!.Result!.Should().NotBeNull(); + _identityDeletionProcessAuditLogsResponse!.ContentType.Should().StartWith("application/json"); + _identityDeletionProcessAuditLogsResponse.Should().ComplyWithSchema(); + } + [Then("the response contains a Deletion Process")] public void ThenTheResponseContainsADeletionProcess() { @@ -97,6 +113,9 @@ public void ThenTheResponseStatusCodeIs(int expectedStatusCode) if (_identityDeletionProcessResponse != null) ((int)_identityDeletionProcessResponse!.Status).Should().Be(expectedStatusCode); + + if (_identityDeletionProcessAuditLogsResponse != null) + ((int)_identityDeletionProcessAuditLogsResponse!.Status).Should().Be(expectedStatusCode); } [Then(@"the response content contains an error with the error code ""([^""]+)""")] diff --git a/BuildingBlocks/src/UnitTestTools/TestDoubles/Fakes/FakeDbContextFactory.cs b/BuildingBlocks/src/UnitTestTools/TestDoubles/Fakes/FakeDbContextFactory.cs index 9378006d14..26c6d5fe3e 100644 --- a/BuildingBlocks/src/UnitTestTools/TestDoubles/Fakes/FakeDbContextFactory.cs +++ b/BuildingBlocks/src/UnitTestTools/TestDoubles/Fakes/FakeDbContextFactory.cs @@ -1,5 +1,4 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; -using Backbone.BuildingBlocks.Infrastructure.Persistence.Database; using FakeItEasy; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -8,8 +7,8 @@ namespace Backbone.UnitTestTools.TestDoubles.Fakes; public static class FakeDbContextFactory { - public static (TContext arrangeContext, TContext assertionContext, TContext actContext) - CreateDbContexts(SqliteConnection? connection = null) where TContext : AbstractDbContextBase + public static (TContext arrangeContext, TContext actContext, TContext assertionContext) + CreateDbContexts(SqliteConnection? connection = null) where TContext : DbContext { connection ??= CreateDbConnection(); connection.Open(); diff --git a/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs b/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs index 8bda7b3851..76b2f9ea0f 100644 --- a/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs +++ b/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs @@ -10,6 +10,7 @@ public IdentityDeletionProcessDetailsDTO(IdentityDeletionProcess process) Id = process.Id; AuditLog = process.AuditLog .Select(e => new IdentityDeletionProcessAuditLogEntryDTO(e)) + .OrderBy(e => e.CreatedAt) .ToList(); Status = process.Status; CreatedAt = process.CreatedAt; diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsQuery.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsQuery.cs new file mode 100644 index 0000000000..5470421532 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsQuery.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAuditLogs; + +public class GetDeletionProcessesAuditLogsQuery : IRequest +{ + public GetDeletionProcessesAuditLogsQuery(string identityAddress) + { + IdentityAddress = identityAddress; + } + + public string IdentityAddress { get; set; } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsResponse.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsResponse.cs new file mode 100644 index 0000000000..22c44e9ffc --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/GetDeletionProcessesAuditLogsResponse.cs @@ -0,0 +1,13 @@ +using Backbone.BuildingBlocks.Application.CQRS.BaseClasses; +using Backbone.Modules.Devices.Application.DTOs; +using Backbone.Modules.Devices.Domain.Entities.Identities; + +namespace Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAuditLogs; + +public class GetDeletionProcessesAuditLogsResponse : CollectionResponseBase +{ + public GetDeletionProcessesAuditLogsResponse(IEnumerable processes) + : base(processes.Select(p => new IdentityDeletionProcessAuditLogEntryDTO(p))) + { + } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs new file mode 100644 index 0000000000..1cf6d20517 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs @@ -0,0 +1,21 @@ +using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using MediatR; + +namespace Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAuditLogs; + +public class Handler : IRequestHandler +{ + private readonly IIdentitiesRepository _identityRepository; + + public Handler(IIdentitiesRepository identityRepository) + { + _identityRepository = identityRepository; + } + + public async Task Handle(GetDeletionProcessesAuditLogsQuery request, CancellationToken cancellationToken) + { + var identityDeletionProcessAuditLogEntries = await _identityRepository.GetIdentityDeletionProcessAuditLogsByAddress(Hasher.HashUtf8(request.IdentityAddress), cancellationToken); + return new GetDeletionProcessesAuditLogsResponse(identityDeletionProcessAuditLogEntries.OrderBy(e => e.CreatedAt)); + } +} diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs b/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs index d93f21d058..1b93b7428b 100644 --- a/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs +++ b/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs @@ -28,4 +28,8 @@ public interface IIdentitiesRepository Task GetDeviceById(DeviceId deviceId, CancellationToken cancellationToken, bool track = false); Task Update(Device device, CancellationToken cancellationToken); #endregion + + #region Deletion Process Audit Logs + Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken); + #endregion } diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs index 12bec066ab..7bec649be0 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs @@ -1,4 +1,5 @@ using System.Data; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; using Backbone.BuildingBlocks.Infrastructure.Persistence.Database; using Backbone.BuildingBlocks.Infrastructure.Persistence.Database.ValueConverters; using Backbone.DevelopmentKit.Identity.ValueObjects; @@ -21,23 +22,33 @@ namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database; public class DevicesDbContext : IdentityDbContext, IDevicesDbContext { - private readonly IServiceProvider? _serviceProvider; private const int MAX_RETRY_COUNT = 50000; - private static readonly TimeSpan MAX_RETRY_DELAY = TimeSpan.FromSeconds(1); private const string SQLSERVER = "Microsoft.EntityFrameworkCore.SqlServer"; private const string POSTGRES = "Npgsql.EntityFrameworkCore.PostgreSQL"; + private static readonly TimeSpan MAX_RETRY_DELAY = TimeSpan.FromSeconds(1); + + private readonly IServiceProvider? _serviceProvider; + private readonly IEventBus _eventBus; - public DevicesDbContext(DbContextOptions options) - : base(options) + public DevicesDbContext(DbContextOptions options) : base(options) { + // This constructor is for EF Core only; initializing the properties with null is therefore not a problem + _eventBus = null!; } - public DevicesDbContext(DbContextOptions options, IServiceProvider serviceProvider) - : base(options) + public DevicesDbContext(DbContextOptions options, IEventBus eventBus) : base(options) + { + _eventBus = eventBus; + } + + public DevicesDbContext(DbContextOptions options, IServiceProvider serviceProvider, IEventBus eventBus) : base(options) { _serviceProvider = serviceProvider; + _eventBus = eventBus; } + public DbSet IdentityDeletionProcessAuditLogs { get; set; } = null!; + public DbSet Identities { get; set; } = null!; public DbSet Devices { get; set; } = null!; diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs index 2319f8439b..67a89cd342 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs @@ -9,7 +9,6 @@ using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Modules.Devices.Infrastructure.Persistence.Database; using Backbone.Modules.Devices.Infrastructure.Persistence.Database.QueryableExtensions; -using Backbone.Tooling; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -23,6 +22,7 @@ public class IdentitiesRepository : IIdentitiesRepository private readonly DbSet _devices; private readonly IQueryable _readonlyDevices; private readonly UserManager _userManager; + private readonly IQueryable _readonlyIdentityDeletionProcessAuditLogs; public IdentitiesRepository(DevicesDbContext dbContext, UserManager userManager) { @@ -31,6 +31,7 @@ public IdentitiesRepository(DevicesDbContext dbContext, UserManager> FindAll(PaginationFilter paginat .FirstWithAddressOrDefault(address, cancellationToken); } + public async Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken) + { + return await _readonlyIdentityDeletionProcessAuditLogs + .Where(auditLog => auditLog.IdentityAddressHash == identityAddressHash) + .ToListAsync(cancellationToken); + } + public async Task Exists(IdentityAddress address, CancellationToken cancellationToken) { return await _readonlyIdentities diff --git a/Modules/Devices/test/Devices.Application.Tests/ApplicationDbContextExtensions.cs b/Modules/Devices/test/Devices.Application.Tests/ApplicationDbContextExtensions.cs index 06e2de5586..abfbd2049e 100644 --- a/Modules/Devices/test/Devices.Application.Tests/ApplicationDbContextExtensions.cs +++ b/Modules/Devices/test/Devices.Application.Tests/ApplicationDbContextExtensions.cs @@ -17,4 +17,10 @@ public static TEntity SaveEntity(this DevicesDbContext dbContext, TEnti dbContext.SaveChanges(); return entity; } + + public static void RemoveEntity(this DevicesDbContext dbContext, TEntity entity) where TEntity : class + { + dbContext.Set().Remove(entity); + dbContext.SaveChanges(); + } } diff --git a/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs b/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs index dfd86ee188..cef63f392c 100644 --- a/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs +++ b/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs @@ -1,3 +1,4 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Devices.Domain.Aggregates.Tier; using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Tooling; @@ -45,6 +46,41 @@ public static Identity CreateIdentityWithOneDevice() return identity; } + public static IdentityDeletionProcess CreateCancelledDeletionProcessFor(Identity identity) + { + var deletionProcess = identity.StartDeletionProcessAsSupport(); + identity.ApproveDeletionProcess(deletionProcess.Id, identity.Devices.First().Id); + identity.CancelDeletionProcessAsSupport(deletionProcess.Id); + + return deletionProcess; + } + + public static IdentityDeletionProcess CreateApprovedDeletionProcessFor(Identity identity, DeviceId deviceId) + { + var deletionProcess = identity.StartDeletionProcessAsOwner(deviceId); + + return deletionProcess; + } + + public static IdentityDeletionProcess CreateRejectedDeletionProcessFor(Identity identity, DeviceId deviceId) + { + var deletionProcess = identity.StartDeletionProcessAsSupport(); + identity.RejectDeletionProcess(deletionProcess.Id, deviceId); + + return deletionProcess; + } + + public static IdentityDeletionProcess CreateDeletingDeletionProcessFor(Identity identity, DeviceId deviceId) + { + var deletionProcess = identity.StartDeletionProcessAsOwner(deviceId); + + SystemTime.Set(SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.LengthOfGracePeriod + 1)); + identity.DeletionStarted(); + SystemTime.UndoSet(); + + return deletionProcess; + } + public static Identity CreateIdentityWithApprovedDeletionProcess(DateTime? approvalDate = null) { approvalDate ??= SystemTime.UtcNow; diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/FindDeletionProcessAuditLogsByAddressStubRepository.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/FindDeletionProcessAuditLogsByAddressStubRepository.cs new file mode 100644 index 0000000000..9259866e15 --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/FindDeletionProcessAuditLogsByAddressStubRepository.cs @@ -0,0 +1,84 @@ +using System.Linq.Expressions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database; +using Backbone.BuildingBlocks.Application.Pagination; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Domain.Entities.Identities; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Queries.GetDeletionProcessesAuditLogs; + +public class FindDeletionProcessAuditLogsByAddressStubRepository : IIdentitiesRepository +{ + private readonly IEnumerable _identityDeletionProcessAuditLogs; + + public FindDeletionProcessAuditLogsByAddressStubRepository(IEnumerable identityDeletionProcessAuditLogs) + { + _identityDeletionProcessAuditLogs = identityDeletionProcessAuditLogs; + } + + public Task Exists(IdentityAddress address, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> FindAllWithDeletionProcessInStatus(DeletionProcessStatus status, CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + + public Task CountByClientId(string clientId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task AddUser(ApplicationUser user, string password) + { + throw new NotImplementedException(); + } + + public Task> FindAll(PaginationFilter paginationFilter, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetDeviceById(DeviceId deviceId, CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + + public Task Update(Device device, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken) + { + return Task.FromResult(_identityDeletionProcessAuditLogs); + } + + public Task Update(Identity identity, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByAddress(IdentityAddress address, CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + + public Task> Find(Expression> filter, CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + + public Task Delete(Expression> filter, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} + diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/HandlerTests.cs new file mode 100644 index 0000000000..a602cc589e --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetDeletionProcessesAuditLogs/HandlerTests.cs @@ -0,0 +1,128 @@ +using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAuditLogs; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Backbone.Modules.Devices.Infrastructure.Persistence.Repository; +using Backbone.UnitTestTools.BaseClasses; +using Backbone.UnitTestTools.TestDoubles.Fakes; +using FakeItEasy; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.AspNetCore.Identity; +using Xunit; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Queries.GetDeletionProcessesAuditLogs; + +public class HandlerTests : AbstractTestsBase +{ + private readonly DevicesDbContext _arrangeDbContext; + private readonly DevicesDbContext _actDbContext; + + public HandlerTests() + { + AssertionScope.Current.FormattingOptions.MaxLines = 1000; + + (_arrangeDbContext, _actDbContext, _) = FakeDbContextFactory.CreateDbContexts(); + } + + [Fact] + public async Task Gets_successfully() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithOneDevice(); + TestDataGenerator.CreateCancelledDeletionProcessFor(identity); + + _arrangeDbContext.SaveEntity(identity); + _arrangeDbContext.RemoveEntity(identity); + + var handler = CreateHandler(_actDbContext); + + // Act + var result = await handler.Handle(new GetDeletionProcessesAuditLogsQuery(identity.Address), CancellationToken.None); + + // Assert + result.Should().HaveCount(identity.DeletionProcesses.SelectMany(d => d.AuditLog).Count()); + } + + [Fact] + public async Task Returns_empty_list_for_non_existent_address() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithOneDevice(); + TestDataGenerator.CreateCancelledDeletionProcessFor(identity); + + _arrangeDbContext.SaveEntity(identity); + + var handler = CreateHandler(_actDbContext); + + // Act + var result = await handler.Handle(new GetDeletionProcessesAuditLogsQuery("non-existent-identity-address"), CancellationToken.None); + + // Assert + result.Should().HaveCount(0); + } + + [Fact] + public async Task Gets_for_multiple_deletion_processes() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithOneDevice(); + TestDataGenerator.CreateCancelledDeletionProcessFor(identity); + TestDataGenerator.CreateRejectedDeletionProcessFor(identity, identity.Devices.First().Id); + TestDataGenerator.CreateApprovedDeletionProcessFor(identity, identity.Devices.First().Id); + + _arrangeDbContext.SaveEntity(identity); + _arrangeDbContext.RemoveEntity(identity); + + var handler = CreateHandler(_actDbContext); + + // Act + var result = await handler.Handle(new GetDeletionProcessesAuditLogsQuery(identity.Address), CancellationToken.None); + + // Assert + result.Should().HaveCount(identity.DeletionProcesses.SelectMany(d => d.AuditLog).Count()); + } + + [Fact] + public async Task Gets_for_deletion_process_in_status_deleting() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithOneDevice(); + TestDataGenerator.CreateDeletingDeletionProcessFor(identity, identity.Devices.First().Id); + + _arrangeDbContext.SaveEntity(identity); + _arrangeDbContext.RemoveEntity(identity); + + var handler = CreateHandler(_actDbContext); + + // Act + var result = await handler.Handle(new GetDeletionProcessesAuditLogsQuery(identity.Address), CancellationToken.None); + + // Assert + result.Should().HaveCount(identity.DeletionProcesses.SelectMany(d => d.AuditLog).Count()); + } + + [Fact] + public async Task Gets_for_deleted_identity() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithOneDevice(); + TestDataGenerator.CreateDeletingDeletionProcessFor(identity, identity.Devices.First().Id); + + _arrangeDbContext.SaveEntity(identity); + _arrangeDbContext.RemoveEntity(identity); + + var handler = CreateHandler(_actDbContext); + + // Act + var result = await handler.Handle(new GetDeletionProcessesAuditLogsQuery(identity.Address), CancellationToken.None); + + // Assert + result.Should().HaveCount(identity.DeletionProcesses.SelectMany(d => d.AuditLog).Count()); + } + + private static Handler CreateHandler(DevicesDbContext actDbContext) + { + var repository = new IdentitiesRepository(actDbContext, A.Fake>()); + return new Handler(repository); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs index 3b20f17cd9..2ab66cc455 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs @@ -41,22 +41,22 @@ public Task> FindAll(PaginationFilter paginationFil throw new NotImplementedException(); } - public Task> FindAllWithDeletionProcessWaitingForApproval(CancellationToken cancellationToken, bool track = false) + public Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) + public Task GetDeviceById(DeviceId deviceId, CancellationToken cancellationToken, bool track = false) { throw new NotImplementedException(); } - public Task GetDeviceById(DeviceId deviceId, CancellationToken cancellationToken, bool track = false) + public Task Update(Device device, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task Update(Device device, CancellationToken cancellationToken) + public Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -71,11 +71,6 @@ public Task Update(Identity identity, CancellationToken cancellationToken) return Task.FromResult((Identity?)_identity); } - public Task> FindAllWithPastDeletionGracePeriod(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task> Find(Expression> filter, CancellationToken cancellationToken, bool track = false) { throw new NotImplementedException(); diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs index 91a0be8c90..48bdf55caa 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs @@ -41,11 +41,6 @@ public Task> FindAll(PaginationFilter paginationFil return Task.FromResult(_identities); } - public Task> FindAllWithDeletionProcessWaitingForApproval(CancellationToken cancellationToken, bool track = false) - { - throw new NotImplementedException(); - } - public Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -61,17 +56,17 @@ public Task Update(Device device, CancellationToken cancellationToken) throw new NotImplementedException(); } - public Task Update(Identity identity, CancellationToken cancellationToken) + public Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task FindByAddress(IdentityAddress address, CancellationToken cancellationToken, bool track = false) + public Task Update(Identity identity, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task> FindAllWithPastDeletionGracePeriod(CancellationToken cancellationToken) + public Task FindByAddress(IdentityAddress address, CancellationToken cancellationToken, bool track = false) { throw new NotImplementedException(); } diff --git a/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs index 18615a8e06..5585ad1dc5 100644 --- a/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs +++ b/Modules/Quotas/test/Quotas.Infrastructure.Tests/Tests/Repositories/MessagesRepositoryTests.cs @@ -41,7 +41,7 @@ public MessagesRepositoryTests() var connection = FakeDbContextFactory.CreateDbConnection(); (_messagesArrangeContext, _, _) = FakeDbContextFactory.CreateDbContexts(connection); - (_, _, _actContext) = FakeDbContextFactory.CreateDbContexts(connection); + (_, _actContext, _) = FakeDbContextFactory.CreateDbContexts(connection); SystemTime.Set(TODAY); } diff --git a/Modules/Relationships/test/Relationships.Application.Tests/Extensions/RelationshipTemplateQueryableExtensionsTests.cs b/Modules/Relationships/test/Relationships.Application.Tests/Extensions/RelationshipTemplateQueryableExtensionsTests.cs index b620252912..3e4f8bee8b 100644 --- a/Modules/Relationships/test/Relationships.Application.Tests/Extensions/RelationshipTemplateQueryableExtensionsTests.cs +++ b/Modules/Relationships/test/Relationships.Application.Tests/Extensions/RelationshipTemplateQueryableExtensionsTests.cs @@ -19,7 +19,7 @@ public class RelationshipTemplateQueryableExtensionsTests : AbstractTestsBase public RelationshipTemplateQueryableExtensionsTests() { - (_arrangeContext, _, _actContext) = FakeDbContextFactory.CreateDbContexts(); + (_arrangeContext, _actContext, _) = FakeDbContextFactory.CreateDbContexts(); } [Fact] diff --git a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/Datawallet/Queries/GetDatawalletModifications/HandlerTests.cs b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/Datawallet/Queries/GetDatawalletModifications/HandlerTests.cs index cc39f20350..1476ac58a5 100644 --- a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/Datawallet/Queries/GetDatawalletModifications/HandlerTests.cs +++ b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/Datawallet/Queries/GetDatawalletModifications/HandlerTests.cs @@ -28,7 +28,7 @@ public HandlerTests() { AssertionScope.Current.FormattingOptions.MaxLines = 1000; - (_arrangeContext, _, _actContext) = FakeDbContextFactory.CreateDbContexts(); + (_arrangeContext, _actContext, _) = FakeDbContextFactory.CreateDbContexts(); _handler = CreateHandler(); }