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