Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/modules/persistence/Elsa.Persistence.MongoDb/Assembly.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Elsa.MongoDb.UnitTests")]
Original file line number Diff line number Diff line change
Expand Up @@ -437,19 +437,24 @@ private IQueryable<TDocument> 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;
}

internal static IQueryable<TDocument> ApplyTenantFilter(IQueryable<TDocument> 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<TDocument> documents)
Expand All @@ -460,7 +465,19 @@ private void ApplyTenantId(IEnumerable<TDocument> 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;

// Preserve explicitly scoped entities and only stamp ambient tenant IDs on unscoped entities.
if (tenantDocument.TenantId != null)
return;

tenantDocument.TenantId = tenantId.EmptyToNull();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Elsa.Common.Multitenancy;
using Elsa.Common.Entities;
using Elsa.KeyValues.Entities;
using Elsa.Persistence.MongoDb.Common;
using Elsa.Persistence.MongoDb.Modules.Runtime;
Expand Down Expand Up @@ -35,4 +36,89 @@ 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<IMongoCollection<TestEntity>>();
mongoCollectionMock.FindOneAndReplaceAsync(
Arg.Any<FilterDefinition<TestEntity>>(),
Arg.Any<TestEntity>(),
Arg.Any<FindOneAndReplaceOptions<TestEntity>>()
)
.Returns(callInfo => callInfo.Arg<TestEntity>());

var tenantAccessorMock = Substitute.For<ITenantAccessor>();
tenantAccessorMock.Tenant.Returns(new Tenant { Id = "tenant-a", Name = "tenant-a" });
var store = new MongoDbStore<TestEntity>(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<IMongoCollection<TestEntity>>();
mongoCollectionMock.FindOneAndReplaceAsync(
Arg.Any<FilterDefinition<TestEntity>>(),
Arg.Any<TestEntity>(),
Arg.Any<FindOneAndReplaceOptions<TestEntity>>()
)
.Returns(callInfo => callInfo.Arg<TestEntity>());

var tenantAccessorMock = Substitute.For<ITenantAccessor>();
tenantAccessorMock.Tenant.Returns(new Tenant { Id = "tenant-a", Name = "tenant-a" });
var store = new MongoDbStore<TestEntity>(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 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 = MongoDbStore<TestEntity>.ApplyTenantFilter(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 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 = MongoDbStore<TestEntity>.ApplyTenantFilter(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;
}
Loading