From c59be5411b6235d98ca29bd5218ba91dcd6668b9 Mon Sep 17 00:00:00 2001 From: Jim Kerslake <39943820+JimKerslake@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:31:13 +0100 Subject: [PATCH 1/3] v8.4 --- src/cloudscribe.Kvp.Models/README.md | 14 +++++ .../cloudscribe.Kvp.Models.csproj | 8 ++- .../README.md | 14 +++++ ...oudscribe.Kvp.Storage.EFCore.Common.csproj | 8 ++- ...loudscribe.Kvp.Storage.EFCore.MSSQL.csproj | 4 +- ...loudscribe.Kvp.Storage.EFCore.MySql.csproj | 4 +- ...cribe.Kvp.Storage.EFCore.PostgreSql.csproj | 6 +- ...oudscribe.Kvp.Storage.EFCore.SQLite.csproj | 4 +- ...loudscribe.Kvp.Storage.EFCore.pgsql.csproj | 4 +- src/cloudscribe.Kvp.Storage.NoDb/README.md | 14 +++++ .../cloudscribe.Kvp.Storage.NoDb.csproj | 6 +- src/cloudscribe.Kvp.Views.BS5/README.md | 14 +++++ .../cloudscribe.Kvp.Views.BS5.csproj | 4 +- src/cloudscribe.UserProperties.Kvp/README.md | 14 +++++ .../cloudscribe.UserProperties.Kvp.csproj | 12 ++-- src/cloudscribe.UserProperties/README.md | 14 +++++ .../cloudscribe.UserProperties.csproj | 8 ++- src/sourceDev.WebApp/sourceDev.WebApp.csproj | 60 +++++++++---------- update_version.ps1 | 6 +- 19 files changed, 157 insertions(+), 61 deletions(-) create mode 100644 src/cloudscribe.Kvp.Models/README.md create mode 100644 src/cloudscribe.Kvp.Storage.EFCore.Common/README.md create mode 100644 src/cloudscribe.Kvp.Storage.NoDb/README.md create mode 100644 src/cloudscribe.Kvp.Views.BS5/README.md create mode 100644 src/cloudscribe.UserProperties.Kvp/README.md create mode 100644 src/cloudscribe.UserProperties/README.md diff --git a/src/cloudscribe.Kvp.Models/README.md b/src/cloudscribe.Kvp.Models/README.md new file mode 100644 index 0000000..26a4f3e --- /dev/null +++ b/src/cloudscribe.Kvp.Models/README.md @@ -0,0 +1,14 @@ +# cloudscribe.Kvp.Models + +Core models for cloudscribe KVP (Key-Value Pair) persistence. Defines interfaces and POCOs for KVP storage providers. + +## Features +- Shared interfaces and POCOs +- Used by all KVP storage providers +- Extensible for custom data + +## Usage +Reference this library in your KVP storage or integration projects. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) for details. diff --git a/src/cloudscribe.Kvp.Models/cloudscribe.Kvp.Models.csproj b/src/cloudscribe.Kvp.Models/cloudscribe.Kvp.Models.csproj index cad21fb..07887a5 100644 --- a/src/cloudscribe.Kvp.Models/cloudscribe.Kvp.Models.csproj +++ b/src/cloudscribe.Kvp.Models/cloudscribe.Kvp.Models.csproj @@ -2,7 +2,7 @@ model classes for key/value storage - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp @@ -12,15 +12,17 @@ https://github.com/cloudscribe/cloudscribe.UserProperties.Kvp.git git + README.md + - - + + diff --git a/src/cloudscribe.Kvp.Storage.EFCore.Common/README.md b/src/cloudscribe.Kvp.Storage.EFCore.Common/README.md new file mode 100644 index 0000000..ee3056c --- /dev/null +++ b/src/cloudscribe.Kvp.Storage.EFCore.Common/README.md @@ -0,0 +1,14 @@ +# cloudscribe.Kvp.Storage.EFCore.Common + +Common EFCore storage library for cloudscribe KVP (Key-Value Pair) persistence. Provides base models and logic to support various EFCore-backed providers. + +## Features +- Shared EFCore models and migration logic +- Used by multiple EFCore storage providers +- Extensible for custom scenarios + +## Usage +Add this library as a dependency to your EFCore-based KVP storage projects. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) for details. diff --git a/src/cloudscribe.Kvp.Storage.EFCore.Common/cloudscribe.Kvp.Storage.EFCore.Common.csproj b/src/cloudscribe.Kvp.Storage.EFCore.Common/cloudscribe.Kvp.Storage.EFCore.Common.csproj index 0a47c77..fdf71f2 100644 --- a/src/cloudscribe.Kvp.Storage.EFCore.Common/cloudscribe.Kvp.Storage.EFCore.Common.csproj +++ b/src/cloudscribe.Kvp.Storage.EFCore.Common/cloudscribe.Kvp.Storage.EFCore.Common.csproj @@ -1,8 +1,8 @@ - Entity Framework Core common classes for cloudscribe.Kvp - 8.3.0 + Entity Framework Core common classes for cloudscribe.Kvp + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;commands;queries;ef @@ -11,10 +11,12 @@ Apache-2.0 https://github.com/cloudscribe/cloudscribe.UserProperties.Kvp.git git + README.md + @@ -25,7 +27,7 @@ - + diff --git a/src/cloudscribe.Kvp.Storage.EFCore.MSSQL/cloudscribe.Kvp.Storage.EFCore.MSSQL.csproj b/src/cloudscribe.Kvp.Storage.EFCore.MSSQL/cloudscribe.Kvp.Storage.EFCore.MSSQL.csproj index c8a2881..c031e0d 100644 --- a/src/cloudscribe.Kvp.Storage.EFCore.MSSQL/cloudscribe.Kvp.Storage.EFCore.MSSQL.csproj +++ b/src/cloudscribe.Kvp.Storage.EFCore.MSSQL/cloudscribe.Kvp.Storage.EFCore.MSSQL.csproj @@ -2,7 +2,7 @@ Entity Framework Core common classes for cloudscribe.Kvp - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;commands;queries;ef @@ -23,7 +23,7 @@ - + diff --git a/src/cloudscribe.Kvp.Storage.EFCore.MySql/cloudscribe.Kvp.Storage.EFCore.MySql.csproj b/src/cloudscribe.Kvp.Storage.EFCore.MySql/cloudscribe.Kvp.Storage.EFCore.MySql.csproj index 12458c5..0adf420 100644 --- a/src/cloudscribe.Kvp.Storage.EFCore.MySql/cloudscribe.Kvp.Storage.EFCore.MySql.csproj +++ b/src/cloudscribe.Kvp.Storage.EFCore.MySql/cloudscribe.Kvp.Storage.EFCore.MySql.csproj @@ -2,7 +2,7 @@ MySql Entity Framework Core storage for cloudscribe.Kvp - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;commands;queries;ef @@ -24,7 +24,7 @@ - + diff --git a/src/cloudscribe.Kvp.Storage.EFCore.PostgreSql/cloudscribe.Kvp.Storage.EFCore.PostgreSql.csproj b/src/cloudscribe.Kvp.Storage.EFCore.PostgreSql/cloudscribe.Kvp.Storage.EFCore.PostgreSql.csproj index 1a2b98a..dd6c7d5 100644 --- a/src/cloudscribe.Kvp.Storage.EFCore.PostgreSql/cloudscribe.Kvp.Storage.EFCore.PostgreSql.csproj +++ b/src/cloudscribe.Kvp.Storage.EFCore.PostgreSql/cloudscribe.Kvp.Storage.EFCore.PostgreSql.csproj @@ -2,7 +2,7 @@ Entity Framework Core postgresql storage for cloudscribe.Kvp - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;commands;ef @@ -24,14 +24,14 @@ - + - + diff --git a/src/cloudscribe.Kvp.Storage.EFCore.SQLite/cloudscribe.Kvp.Storage.EFCore.SQLite.csproj b/src/cloudscribe.Kvp.Storage.EFCore.SQLite/cloudscribe.Kvp.Storage.EFCore.SQLite.csproj index 4da81fc..0d88c8f 100644 --- a/src/cloudscribe.Kvp.Storage.EFCore.SQLite/cloudscribe.Kvp.Storage.EFCore.SQLite.csproj +++ b/src/cloudscribe.Kvp.Storage.EFCore.SQLite/cloudscribe.Kvp.Storage.EFCore.SQLite.csproj @@ -2,7 +2,7 @@ Entity Framework Core SQLite storage for cloudscribe.Kvp - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;ef @@ -23,7 +23,7 @@ - + diff --git a/src/cloudscribe.Kvp.Storage.EFCore.pgsql/cloudscribe.Kvp.Storage.EFCore.pgsql.csproj b/src/cloudscribe.Kvp.Storage.EFCore.pgsql/cloudscribe.Kvp.Storage.EFCore.pgsql.csproj index 29ce7aa..b538de1 100644 --- a/src/cloudscribe.Kvp.Storage.EFCore.pgsql/cloudscribe.Kvp.Storage.EFCore.pgsql.csproj +++ b/src/cloudscribe.Kvp.Storage.EFCore.pgsql/cloudscribe.Kvp.Storage.EFCore.pgsql.csproj @@ -2,7 +2,7 @@ Entity Framework Core postgresql storage for cloudscribe.Kvp - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;commands;ef @@ -24,7 +24,7 @@ - + diff --git a/src/cloudscribe.Kvp.Storage.NoDb/README.md b/src/cloudscribe.Kvp.Storage.NoDb/README.md new file mode 100644 index 0000000..ff18086 --- /dev/null +++ b/src/cloudscribe.Kvp.Storage.NoDb/README.md @@ -0,0 +1,14 @@ +# cloudscribe.Kvp.Storage.NoDb + +NoDb storage provider for cloudscribe KVP (Key-Value Pair) persistence. Enables lightweight, file-based storage for development or small-scale usage. + +## Features +- Simple, file-based KVP storage +- No database server required +- Ideal for development or small deployments + +## Usage +Add this package to your project and configure NoDb as your KVP storage provider. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) for details. diff --git a/src/cloudscribe.Kvp.Storage.NoDb/cloudscribe.Kvp.Storage.NoDb.csproj b/src/cloudscribe.Kvp.Storage.NoDb/cloudscribe.Kvp.Storage.NoDb.csproj index d8b003c..ef55d53 100644 --- a/src/cloudscribe.Kvp.Storage.NoDb/cloudscribe.Kvp.Storage.NoDb.csproj +++ b/src/cloudscribe.Kvp.Storage.NoDb/cloudscribe.Kvp.Storage.NoDb.csproj @@ -2,7 +2,7 @@ NoDb storage for cloudscribe key/value models. NoDb storage is only recommended for small sites. - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;kvp;nodb @@ -11,14 +11,16 @@ Apache-2.0 https://github.com/cloudscribe/cloudscribe.UserProperties.Kvp.git git + README.md + - + diff --git a/src/cloudscribe.Kvp.Views.BS5/README.md b/src/cloudscribe.Kvp.Views.BS5/README.md new file mode 100644 index 0000000..ca60714 --- /dev/null +++ b/src/cloudscribe.Kvp.Views.BS5/README.md @@ -0,0 +1,14 @@ +# cloudscribe.Kvp.Views.BS5 + +Bootstrap 5 views for cloudscribe KVP (Key-Value Pair) management. Provides modern, responsive UI components for KVP admin and user interaction. + +## Features +- Bootstrap 5-based Razor views +- User-friendly admin and data entry screens +- Easily customizable for your app + +## Usage +Add this package to your ASP.NET Core MVC app and use the provided Razor views for KVP management. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) for details. diff --git a/src/cloudscribe.Kvp.Views.BS5/cloudscribe.Kvp.Views.BS5.csproj b/src/cloudscribe.Kvp.Views.BS5/cloudscribe.Kvp.Views.BS5.csproj index 2734054..54d7836 100644 --- a/src/cloudscribe.Kvp.Views.BS5/cloudscribe.Kvp.Views.BS5.csproj +++ b/src/cloudscribe.Kvp.Views.BS5/cloudscribe.Kvp.Views.BS5.csproj @@ -3,7 +3,7 @@ net8.0 true - 8.3.0 + 8.4.0 Custom views for cloudscribe.Kvp cloudscribe;kvp;commands;ef icon.png @@ -12,10 +12,12 @@ Apache-2.0 https://github.com/cloudscribe/cloudscribe.UserProperties.Kvp.git git + README.md + diff --git a/src/cloudscribe.UserProperties.Kvp/README.md b/src/cloudscribe.UserProperties.Kvp/README.md new file mode 100644 index 0000000..b9b4368 --- /dev/null +++ b/src/cloudscribe.UserProperties.Kvp/README.md @@ -0,0 +1,14 @@ +# cloudscribe.UserProperties.Kvp + +Integration library for connecting cloudscribe.UserProperties with KVP storage providers. Enables flexible, scalable user property storage using the KVP pattern. + +## Features +- Connects user property management to KVP storage +- Supports multiple backend providers +- Extensible integration logic + +## Usage +Add this package to your project to enable KVP-backed user property storage in cloudscribe-based apps. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) for details. diff --git a/src/cloudscribe.UserProperties.Kvp/cloudscribe.UserProperties.Kvp.csproj b/src/cloudscribe.UserProperties.Kvp/cloudscribe.UserProperties.Kvp.csproj index 3c7c604..6aae593 100644 --- a/src/cloudscribe.UserProperties.Kvp/cloudscribe.UserProperties.Kvp.csproj +++ b/src/cloudscribe.UserProperties.Kvp/cloudscribe.UserProperties.Kvp.csproj @@ -2,7 +2,7 @@ Configurable custom user properties for cloudscribe core using per tenant or global configuration based custom proprties with key/value storage - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;userprofile;customization @@ -11,10 +11,12 @@ Apache-2.0 https://github.com/cloudscribe/cloudscribe.UserProperties.Kvp.git git + README.md + @@ -22,10 +24,10 @@ - - - - + + + + diff --git a/src/cloudscribe.UserProperties/README.md b/src/cloudscribe.UserProperties/README.md new file mode 100644 index 0000000..e308233 --- /dev/null +++ b/src/cloudscribe.UserProperties/README.md @@ -0,0 +1,14 @@ +# cloudscribe.UserProperties + +User property management for cloudscribe-based applications. Provides APIs and storage integration for user profile properties. + +## Features +- Add custom properties to user profiles +- Integrates with cloudscribe KVP storage +- Extensible and customizable + +## Usage +Reference this library in your cloudscribe-based project to enable user property management. + +## License +Licensed under the Apache License, Version 2.0. See [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) for details. diff --git a/src/cloudscribe.UserProperties/cloudscribe.UserProperties.csproj b/src/cloudscribe.UserProperties/cloudscribe.UserProperties.csproj index d287e8f..7a23a7e 100644 --- a/src/cloudscribe.UserProperties/cloudscribe.UserProperties.csproj +++ b/src/cloudscribe.UserProperties/cloudscribe.UserProperties.csproj @@ -2,7 +2,7 @@ Models for custom user properties that can be configured per tenant for cloudscribe core customization - 8.3.0 + 8.4.0 net8.0 Joe Audette cloudscribe;userprofile;customization @@ -11,10 +11,12 @@ Apache-2.0 https://github.com/cloudscribe/cloudscribe.UserProperties.Kvp.git git + README.md + @@ -22,8 +24,8 @@ - - + + diff --git a/src/sourceDev.WebApp/sourceDev.WebApp.csproj b/src/sourceDev.WebApp/sourceDev.WebApp.csproj index d131ad2..bb3848b 100644 --- a/src/sourceDev.WebApp/sourceDev.WebApp.csproj +++ b/src/sourceDev.WebApp/sourceDev.WebApp.csproj @@ -48,41 +48,41 @@ - - - + + + - - - - - - + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - + + + + + + + - + diff --git a/update_version.ps1 b/update_version.ps1 index 32134c7..0ed4dd0 100644 --- a/update_version.ps1 +++ b/update_version.ps1 @@ -16,9 +16,9 @@ $directory = "src" # Define the old & new versions -$oldVersion = '8\.2' # slash needed ! -$newVersion = "8.3.0" -$newWildcardVersion = "8.3.*" +$oldVersion = '8\.3' # slash needed ! +$newVersion = "8.4.0" +$newWildcardVersion = "8.4.*" # Get all .csproj files in the directory and subdirectories From e201d1cc8e2b7cbdfbaa7b3ab25e8521b162abcb Mon Sep 17 00:00:00 2001 From: Jim Kerslake <39943820+JimKerslake@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:28:08 +0100 Subject: [PATCH 2/3] #45 clear KVPs on successful user delete --- .../KvpUserPostDeleteHandler.cs | 86 +++++++++++++++++++ .../StartupExtensions.cs | 7 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/cloudscribe.UserProperties.Kvp/KvpUserPostDeleteHandler.cs diff --git a/src/cloudscribe.UserProperties.Kvp/KvpUserPostDeleteHandler.cs b/src/cloudscribe.UserProperties.Kvp/KvpUserPostDeleteHandler.cs new file mode 100644 index 0000000..336cf93 --- /dev/null +++ b/src/cloudscribe.UserProperties.Kvp/KvpUserPostDeleteHandler.cs @@ -0,0 +1,86 @@ +// Copyright (c) Source Tree Solutions, LLC. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Author: Joe Audette +// Created: 2025-08-13 +// Last Modified: 2025-08-13 +// + +using cloudscribe.Core.Models.EventHandlers; +using cloudscribe.Kvp.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace cloudscribe.UserProperties.Kvp +{ + /// + /// Handles cleanup of user KVP data after successful user deletion. + /// This handler only executes if the user deletion was successful, ensuring transactional safety. + /// + public class KvpUserPostDeleteHandler : IHandleUserPostDelete + { + public KvpUserPostDeleteHandler( + IKvpStorageService kvpStorage, + ILogger logger) + { + _kvpStorage = kvpStorage; + _logger = logger; + } + + private readonly IKvpStorageService _kvpStorage; + private readonly ILogger _logger; + + public async Task HandleUserPostDelete( + Guid siteId, + Guid userId, + CancellationToken cancellationToken = default(CancellationToken)) + { + _logger.LogInformation("Starting KVP cleanup for deleted user {UserId} in site {SiteId}", userId, siteId); + + try + { + // Fetch all KVP items for this user + // User KVPs are stored with SubSetId = userId + var userKvps = await _kvpStorage.FetchById( + siteId.ToString(), // projectId + "*", // featureId (all features) + siteId.ToString(), // setId (site-scoped) + userId.ToString(), // subSetId (user-scoped) + cancellationToken).ConfigureAwait(false); + + if (userKvps.Count == 0) + { + _logger.LogDebug("No KVP data found for user {UserId}", userId); + return; + } + + // Delete each KVP item individually + int deletedCount = 0; + foreach (var kvp in userKvps) + { + try + { + await _kvpStorage.Delete(siteId.ToString(), kvp.Id, cancellationToken).ConfigureAwait(false); + deletedCount++; + _logger.LogDebug("Deleted KVP item {KvpId} (key: {Key}) for user {UserId}", kvp.Id, kvp.Key, userId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete KVP item {KvpId} (key: {Key}) for user {UserId}", kvp.Id, kvp.Key, userId); + // Continue with other items even if one fails + } + } + + _logger.LogInformation("Successfully cleaned up {DeletedCount} of {TotalCount} KVP items for user {UserId}", + deletedCount, userKvps.Count, userId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to cleanup KVP data for user {UserId} in site {SiteId}", userId, siteId); + // Don't throw - let other post-delete handlers continue + // The user has already been successfully deleted from the main system + } + } + } +} \ No newline at end of file diff --git a/src/cloudscribe.UserProperties.Kvp/StartupExtensions.cs b/src/cloudscribe.UserProperties.Kvp/StartupExtensions.cs index ccfad51..c0fa2f7 100644 --- a/src/cloudscribe.UserProperties.Kvp/StartupExtensions.cs +++ b/src/cloudscribe.UserProperties.Kvp/StartupExtensions.cs @@ -1,4 +1,5 @@ -using cloudscribe.Core.Web.ExtensionPoints; +using cloudscribe.Core.Models.EventHandlers; +using cloudscribe.Core.Web.ExtensionPoints; using cloudscribe.Kvp.Models; using cloudscribe.UserProperties.Models; using cloudscribe.UserProperties.Services; @@ -21,6 +22,10 @@ public static IServiceCollection AddCloudscribeKvpUserProperties(this IServiceCo services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + + // Register post-delete handler for automatic KVP cleanup when users are deleted + services.TryAddScoped(); + services.AddScoped(); From 5b8c0cb0e36ee7b0e60fb14ff4546e0399cc81af Mon Sep 17 00:00:00 2001 From: Jim Kerslake <39943820+JimKerslake@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:40:58 +0100 Subject: [PATCH 3/3] #45 unit tests --- cloudscribe.UserProperties.Kvp.sln | 17 ++ .../KvpUserPostDeleteHandlerTests.cs | 249 ++++++++++++++++++ .../cloudscribe.Kvp.UnitTests.csproj | 33 +++ 3 files changed, 299 insertions(+) create mode 100644 tests/cloudscribe.Kvp.UnitTests/KvpUserPostDeleteHandlerTests.cs create mode 100644 tests/cloudscribe.Kvp.UnitTests/cloudscribe.Kvp.UnitTests.csproj diff --git a/cloudscribe.UserProperties.Kvp.sln b/cloudscribe.UserProperties.Kvp.sln index 715a2ca..515d247 100644 --- a/cloudscribe.UserProperties.Kvp.sln +++ b/cloudscribe.UserProperties.Kvp.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{26B35914-6144-495B-AE28-CF5E21CA3B94}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8B2A2B3C-4D5E-6F7A-8B9C-0D1E2F3A4B5C}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cloudscribe.Kvp.Models", "src\cloudscribe.Kvp.Models\cloudscribe.Kvp.Models.csproj", "{C2AB7956-D813-4F6B-839E-0BDBECD3733F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cloudscribe.Kvp.Storage.EFCore.Common", "src\cloudscribe.Kvp.Storage.EFCore.Common\cloudscribe.Kvp.Storage.EFCore.Common.csproj", "{26280B75-9A2F-43C2-BCA2-2E1828C72B27}" @@ -29,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sourceDev.WebApp", "src\sou EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cloudscribe.Kvp.Views.BS5", "src\cloudscribe.Kvp.Views.BS5\cloudscribe.Kvp.Views.BS5.csproj", "{CCFCCC62-B27E-4CC5-979F-A41E9988B018}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cloudscribe.Kvp.UnitTests", "tests\cloudscribe.Kvp.UnitTests\cloudscribe.Kvp.UnitTests.csproj", "{D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -183,6 +187,18 @@ Global {CCFCCC62-B27E-4CC5-979F-A41E9988B018}.Release|x64.Build.0 = Release|Any CPU {CCFCCC62-B27E-4CC5-979F-A41E9988B018}.Release|x86.ActiveCfg = Release|Any CPU {CCFCCC62-B27E-4CC5-979F-A41E9988B018}.Release|x86.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Debug|x64.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Debug|x86.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Release|x64.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Release|x64.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Release|x86.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -200,6 +216,7 @@ Global {DABDCF8A-5FBC-4192-99F5-3FE9125FF2D9} = {26B35914-6144-495B-AE28-CF5E21CA3B94} {2C31220E-680F-4F1E-9950-0F004CDA3904} = {26B35914-6144-495B-AE28-CF5E21CA3B94} {CCFCCC62-B27E-4CC5-979F-A41E9988B018} = {26B35914-6144-495B-AE28-CF5E21CA3B94} + {D4E5F6A7-B8C9-4D0E-1F2A-3B4C5D6E7F8A} = {8B2A2B3C-4D5E-6F7A-8B9C-0D1E2F3A4B5C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B5E647E9-136D-490E-A9FF-8E895ECC87E4} diff --git a/tests/cloudscribe.Kvp.UnitTests/KvpUserPostDeleteHandlerTests.cs b/tests/cloudscribe.Kvp.UnitTests/KvpUserPostDeleteHandlerTests.cs new file mode 100644 index 0000000..4f47d46 --- /dev/null +++ b/tests/cloudscribe.Kvp.UnitTests/KvpUserPostDeleteHandlerTests.cs @@ -0,0 +1,249 @@ +using cloudscribe.Kvp.Models; +using cloudscribe.UserProperties.Kvp; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace cloudscribe.Kvp.UnitTests +{ + public class KvpUserPostDeleteHandlerTests + { + private readonly Mock _mockKvpStorage; + private readonly Mock> _mockLogger; + private readonly KvpUserPostDeleteHandler _handler; + private readonly Guid _siteId = Guid.NewGuid(); + private readonly Guid _userId = Guid.NewGuid(); + + public KvpUserPostDeleteHandlerTests() + { + _mockKvpStorage = new Mock(); + _mockLogger = new Mock>(); + _handler = new KvpUserPostDeleteHandler(_mockKvpStorage.Object, _mockLogger.Object); + } + + [Fact] + public async Task HandleUserPostDelete_WithUserKvpItems_DeletesAllItems() + { + // Arrange + var kvpItems = new List + { + CreateMockKvpItem("1", "FirstName", "John"), + CreateMockKvpItem("2", "LastName", "Doe"), + CreateMockKvpItem("3", "MembershipNo", "12345") + }; + + _mockKvpStorage + .Setup(x => x.FetchById( + _siteId.ToString(), + "*", + _siteId.ToString(), + _userId.ToString(), + It.IsAny())) + .ReturnsAsync(kvpItems); + + _mockKvpStorage + .Setup(x => x.Delete(_siteId.ToString(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleUserPostDelete(_siteId, _userId, CancellationToken.None); + + // Assert + _mockKvpStorage.Verify( + x => x.FetchById( + _siteId.ToString(), + "*", + _siteId.ToString(), + _userId.ToString(), + It.IsAny()), + Times.Once); + + _mockKvpStorage.Verify( + x => x.Delete(_siteId.ToString(), "1", It.IsAny()), + Times.Once); + + _mockKvpStorage.Verify( + x => x.Delete(_siteId.ToString(), "2", It.IsAny()), + Times.Once); + + _mockKvpStorage.Verify( + x => x.Delete(_siteId.ToString(), "3", It.IsAny()), + Times.Once); + + VerifyLogMessage(LogLevel.Information, "Starting KVP cleanup for deleted user"); + VerifyLogMessage(LogLevel.Information, "Successfully cleaned up 3 of 3 KVP items for user"); + } + + [Fact] + public async Task HandleUserPostDelete_WithNoKvpItems_LogsDebugMessage() + { + // Arrange + _mockKvpStorage + .Setup(x => x.FetchById( + _siteId.ToString(), + "*", + _siteId.ToString(), + _userId.ToString(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act + await _handler.HandleUserPostDelete(_siteId, _userId, CancellationToken.None); + + // Assert + _mockKvpStorage.Verify( + x => x.Delete(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + VerifyLogMessage(LogLevel.Debug, "No KVP data found for user"); + } + + [Fact] + public async Task HandleUserPostDelete_WhenFetchThrowsException_LogsErrorAndDoesNotThrow() + { + // Arrange + _mockKvpStorage + .Setup(x => x.FetchById( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new InvalidOperationException("Database connection failed")); + + // Act & Assert + var exception = await Record.ExceptionAsync(async () => + await _handler.HandleUserPostDelete(_siteId, _userId, CancellationToken.None)); + + // Should not throw - errors should be caught and logged + Assert.Null(exception); + + VerifyLogMessage(LogLevel.Error, "Failed to cleanup KVP data for user"); + } + + [Fact] + public async Task HandleUserPostDelete_WhenDeleteThrowsException_ContinuesWithOtherItems() + { + // Arrange + var kvpItems = new List + { + CreateMockKvpItem("1", "FirstName", "John"), + CreateMockKvpItem("2", "LastName", "Doe"), + CreateMockKvpItem("3", "MembershipNo", "12345") + }; + + _mockKvpStorage + .Setup(x => x.FetchById( + _siteId.ToString(), + "*", + _siteId.ToString(), + _userId.ToString(), + It.IsAny())) + .ReturnsAsync(kvpItems); + + // First delete succeeds + _mockKvpStorage + .Setup(x => x.Delete(_siteId.ToString(), "1", It.IsAny())) + .Returns(Task.CompletedTask); + + // Second delete fails + _mockKvpStorage + .Setup(x => x.Delete(_siteId.ToString(), "2", It.IsAny())) + .ThrowsAsync(new InvalidOperationException("Delete failed")); + + // Third delete succeeds + _mockKvpStorage + .Setup(x => x.Delete(_siteId.ToString(), "3", It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var exception = await Record.ExceptionAsync(async () => + await _handler.HandleUserPostDelete(_siteId, _userId, CancellationToken.None)); + + // Assert + Assert.Null(exception); // Should not throw + + // Verify all delete attempts were made + _mockKvpStorage.Verify( + x => x.Delete(_siteId.ToString(), "1", It.IsAny()), + Times.Once); + + _mockKvpStorage.Verify( + x => x.Delete(_siteId.ToString(), "2", It.IsAny()), + Times.Once); + + _mockKvpStorage.Verify( + x => x.Delete(_siteId.ToString(), "3", It.IsAny()), + Times.Once); + + // Should log success message with partial cleanup count + VerifyLogMessage(LogLevel.Information, "Successfully cleaned up 2 of 3 KVP items for user"); + VerifyLogMessage(LogLevel.Error, "Failed to delete KVP item"); + } + + [Fact] + public async Task HandleUserPostDelete_UsesCancellationToken() + { + // Arrange + var cancellationToken = new CancellationToken(); + var kvpItems = new List + { + CreateMockKvpItem("1", "FirstName", "John") + }; + + _mockKvpStorage + .Setup(x => x.FetchById( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + cancellationToken)) + .ReturnsAsync(kvpItems); + + _mockKvpStorage + .Setup(x => x.Delete(It.IsAny(), It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask); + + // Act + await _handler.HandleUserPostDelete(_siteId, _userId, cancellationToken); + + // Assert + _mockKvpStorage.Verify( + x => x.FetchById( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + cancellationToken), + Times.Once); + + _mockKvpStorage.Verify( + x => x.Delete(It.IsAny(), It.IsAny(), cancellationToken), + Times.Once); + } + + + private IKvpItem CreateMockKvpItem(string id, string key, string value) + { + var mock = new Mock(); + mock.Setup(x => x.Id).Returns(id); + mock.Setup(x => x.Key).Returns(key); + mock.Setup(x => x.Value).Returns(value); + mock.Setup(x => x.SetId).Returns(_siteId.ToString()); + mock.Setup(x => x.SubSetId).Returns(_userId.ToString()); + return mock.Object; + } + + private void VerifyLogMessage(LogLevel level, string messageContains) + { + _mockLogger.Verify( + logger => logger.Log( + level, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(messageContains)), + It.IsAny(), + It.IsAny>()), + Times.AtLeastOnce); + } + } +} \ No newline at end of file diff --git a/tests/cloudscribe.Kvp.UnitTests/cloudscribe.Kvp.UnitTests.csproj b/tests/cloudscribe.Kvp.UnitTests/cloudscribe.Kvp.UnitTests.csproj new file mode 100644 index 0000000..8973f57 --- /dev/null +++ b/tests/cloudscribe.Kvp.UnitTests/cloudscribe.Kvp.UnitTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + \ No newline at end of file