From 2ad3a77f4e11e9c7cf43d9b240ca39e7c53e0dab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:12:32 +0000 Subject: [PATCH 1/3] Initial plan From 577d5a19c021de0bebcc022030fdca78e6b8d82d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:16:20 +0000 Subject: [PATCH 2/3] fix mongodb tenant-agnostic workflow definition visibility --- .../Common/MongoDbStore.cs | 22 ++++- .../MongoKeyValueStoreTests.cs | 93 +++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs b/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs index af4371a0..bb6bc95f 100644 --- a/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs @@ -437,19 +437,24 @@ private IQueryable GetQueryableCollection(bool tenantAgnostic = false { var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id.EmptyToNull(); - queryable = queryable.Where(x => (x as Entity)!.TenantId == tenantId); + queryable = ApplyTenantFilter(queryable, tenantId); } return queryable; } + private static IQueryable ApplyTenantFilter(IQueryable queryable, string? tenantId) + { + return queryable.Where(x => (x as Entity)!.TenantId == tenantId || (x as Entity)!.TenantId == Tenant.AgnosticTenantId); + } + private void ApplyTenantId(TDocument document) { var tenant = tenantAccessor.Tenant; var tenantId = tenant?.Id; if (document is Entity tenantDocument) - tenantDocument.TenantId = tenantId.EmptyToNull(); + ApplyTenantId(tenantDocument, tenantId); } private void ApplyTenantId(IEnumerable documents) @@ -460,7 +465,18 @@ private void ApplyTenantId(IEnumerable documents) foreach (var document in documents) { if (document is Entity tenantDocument) - tenantDocument.TenantId = tenantId.EmptyToNull(); + ApplyTenantId(tenantDocument, tenantId); } } + + private static void ApplyTenantId(Entity tenantDocument, string? tenantId) + { + if (tenantDocument.TenantId == Tenant.AgnosticTenantId) + return; + + if (tenantDocument.TenantId != null) + return; + + tenantDocument.TenantId = tenantId.EmptyToNull(); + } } diff --git a/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs b/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs index 904b7972..1d68a192 100644 --- a/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs +++ b/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs @@ -1,9 +1,11 @@ using Elsa.Common.Multitenancy; +using Elsa.Common.Entities; using Elsa.KeyValues.Entities; using Elsa.Persistence.MongoDb.Common; using Elsa.Persistence.MongoDb.Modules.Runtime; using MongoDB.Driver; using NSubstitute; +using System.Reflection; namespace Elsa.MongoDb.UnitTests; @@ -35,4 +37,95 @@ public async Task SaveAsync_WithSerializedKeyValuePairDocument_DoesNotThrowExcep Assert.Null(exception); } +} + +public class MongoDbStoreTenantTests +{ + [Fact(DisplayName = "When saving an agnostic entity, preserve the agnostic tenant marker")] + public async Task SaveAsync_WithAgnosticTenant_PreservesAgnosticMarker() + { + var mongoCollectionMock = Substitute.For>(); + mongoCollectionMock.FindOneAndReplaceAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any>() + ) + .Returns(callInfo => callInfo.Arg()); + + var tenantAccessorMock = Substitute.For(); + tenantAccessorMock.Tenant.Returns(new Tenant { Id = "tenant-a", Name = "tenant-a" }); + var store = new MongoDbStore(mongoCollectionMock, tenantAccessorMock); + var entity = new TestEntity { Id = "1", TenantId = Tenant.AgnosticTenantId }; + + await store.SaveAsync(entity); + + Assert.Equal(Tenant.AgnosticTenantId, entity.TenantId); + } + + [Fact(DisplayName = "When saving an unscoped entity under a tenant, stamp ambient tenant")] + public async Task SaveAsync_WithNullTenant_StampsAmbientTenant() + { + var mongoCollectionMock = Substitute.For>(); + mongoCollectionMock.FindOneAndReplaceAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any>() + ) + .Returns(callInfo => callInfo.Arg()); + + var tenantAccessorMock = Substitute.For(); + tenantAccessorMock.Tenant.Returns(new Tenant { Id = "tenant-a", Name = "tenant-a" }); + var store = new MongoDbStore(mongoCollectionMock, tenantAccessorMock); + var entity = new TestEntity { Id = "1", TenantId = null }; + + await store.SaveAsync(entity); + + Assert.Equal("tenant-a", entity.TenantId); + } + + [Fact(DisplayName = "Tenant filter includes tenant-specific and agnostic records")] + public void ApplyTenantFilter_WithConcreteTenant_IncludesAgnostic() + { + var method = typeof(MongoDbStore).GetMethod("ApplyTenantFilter", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(method); + + var documents = new[] + { + new TestEntity { Id = "specific", TenantId = "tenant-a" }, + new TestEntity { Id = "agnostic", TenantId = Tenant.AgnosticTenantId }, + new TestEntity { Id = "other", TenantId = "tenant-b" }, + new TestEntity { Id = "null", TenantId = null } + }.AsQueryable(); + + var filtered = (IQueryable)method!.Invoke(null, [documents, "tenant-a"])!; + var ids = filtered.Select(x => x.Id).ToList(); + + Assert.Contains("specific", ids); + Assert.Contains("agnostic", ids); + Assert.DoesNotContain("other", ids); + Assert.DoesNotContain("null", ids); + } + + [Fact(DisplayName = "Tenant filter includes null and agnostic when ambient tenant is agnostic")] + public void ApplyTenantFilter_WithAgnosticTenant_IncludesNullAndAgnostic() + { + var method = typeof(MongoDbStore).GetMethod("ApplyTenantFilter", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(method); + + var documents = new[] + { + new TestEntity { Id = "specific", TenantId = "tenant-a" }, + new TestEntity { Id = "agnostic", TenantId = Tenant.AgnosticTenantId }, + new TestEntity { Id = "null", TenantId = null } + }.AsQueryable(); + + var filtered = (IQueryable)method!.Invoke(null, [documents, null])!; + var ids = filtered.Select(x => x.Id).ToList(); + + Assert.Contains("agnostic", ids); + Assert.Contains("null", ids); + Assert.DoesNotContain("specific", ids); + } + + public class TestEntity : Entity; } \ No newline at end of file From f69a37db579c566a483aa482d13d97951d76be66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:20:30 +0000 Subject: [PATCH 3/3] test and refine mongodb tenant-agnostic tenancy fix --- .../persistence/Elsa.Persistence.MongoDb/Assembly.cs | 3 +++ .../Elsa.Persistence.MongoDb/Common/MongoDbStore.cs | 3 ++- .../Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs | 11 ++--------- 3 files changed, 7 insertions(+), 10 deletions(-) create mode 100644 src/modules/persistence/Elsa.Persistence.MongoDb/Assembly.cs diff --git a/src/modules/persistence/Elsa.Persistence.MongoDb/Assembly.cs b/src/modules/persistence/Elsa.Persistence.MongoDb/Assembly.cs new file mode 100644 index 00000000..37aa2a17 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.MongoDb/Assembly.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Elsa.MongoDb.UnitTests")] diff --git a/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs b/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs index bb6bc95f..98819401 100644 --- a/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/persistence/Elsa.Persistence.MongoDb/Common/MongoDbStore.cs @@ -443,7 +443,7 @@ private IQueryable GetQueryableCollection(bool tenantAgnostic = false return queryable; } - private static IQueryable ApplyTenantFilter(IQueryable queryable, string? tenantId) + internal static IQueryable ApplyTenantFilter(IQueryable queryable, string? tenantId) { return queryable.Where(x => (x as Entity)!.TenantId == tenantId || (x as Entity)!.TenantId == Tenant.AgnosticTenantId); } @@ -474,6 +474,7 @@ private static void ApplyTenantId(Entity tenantDocument, string? tenantId) if (tenantDocument.TenantId == Tenant.AgnosticTenantId) return; + // Preserve explicitly scoped entities and only stamp ambient tenant IDs on unscoped entities. if (tenantDocument.TenantId != null) return; diff --git a/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs b/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs index 1d68a192..f826fd86 100644 --- a/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs +++ b/test/modules/persistence/Elsa.MongoDb.UnitTests/MongoKeyValueStoreTests.cs @@ -5,7 +5,6 @@ using Elsa.Persistence.MongoDb.Modules.Runtime; using MongoDB.Driver; using NSubstitute; -using System.Reflection; namespace Elsa.MongoDb.UnitTests; @@ -86,9 +85,6 @@ public async Task SaveAsync_WithNullTenant_StampsAmbientTenant() [Fact(DisplayName = "Tenant filter includes tenant-specific and agnostic records")] public void ApplyTenantFilter_WithConcreteTenant_IncludesAgnostic() { - var method = typeof(MongoDbStore).GetMethod("ApplyTenantFilter", BindingFlags.NonPublic | BindingFlags.Static); - Assert.NotNull(method); - var documents = new[] { new TestEntity { Id = "specific", TenantId = "tenant-a" }, @@ -97,7 +93,7 @@ public void ApplyTenantFilter_WithConcreteTenant_IncludesAgnostic() new TestEntity { Id = "null", TenantId = null } }.AsQueryable(); - var filtered = (IQueryable)method!.Invoke(null, [documents, "tenant-a"])!; + var filtered = MongoDbStore.ApplyTenantFilter(documents, "tenant-a"); var ids = filtered.Select(x => x.Id).ToList(); Assert.Contains("specific", ids); @@ -109,9 +105,6 @@ public void ApplyTenantFilter_WithConcreteTenant_IncludesAgnostic() [Fact(DisplayName = "Tenant filter includes null and agnostic when ambient tenant is agnostic")] public void ApplyTenantFilter_WithAgnosticTenant_IncludesNullAndAgnostic() { - var method = typeof(MongoDbStore).GetMethod("ApplyTenantFilter", BindingFlags.NonPublic | BindingFlags.Static); - Assert.NotNull(method); - var documents = new[] { new TestEntity { Id = "specific", TenantId = "tenant-a" }, @@ -119,7 +112,7 @@ public void ApplyTenantFilter_WithAgnosticTenant_IncludesNullAndAgnostic() new TestEntity { Id = "null", TenantId = null } }.AsQueryable(); - var filtered = (IQueryable)method!.Invoke(null, [documents, null])!; + var filtered = MongoDbStore.ApplyTenantFilter(documents, null); var ids = filtered.Select(x => x.Id).ToList(); Assert.Contains("agnostic", ids);