elsa-core/test/unit/Elsa.Common.UnitTests/Multitenancy/TenantResolverContextTests.cs
Sipke Schoorstra b09a564812
Fix Multitenancy Support and Normalize Tenant ID Handling (#7217)
* Enable multitenancy support and normalize tenant ID handling.

- Activate multitenancy in `Program.cs`.
- Introduce `NormalizeTenantId` method for consistent tenant ID usage.
- Update tenant-related classes and features to support normalization logic.

* Add ADR for adopting empty string as the default tenant ID

- Standardized the tenant ID for the default tenant to use an empty string (`""`) instead of `null`.
- Documented the rationale and migration considerations in ADR 0007.
- Updated ADR table of contents and graph for new entry.

* Apply suggestion from @sfmskywalker

* Update doc/adr/graph.dot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Normalize spacing and improve readability in `Program.cs`. Fix multitenancy condition formatting.

* Fix ADR numbering and update TOC

* Add ADRs for flowchart execution model, tenant deletion event, merge modes, and default tenant ID

- Introduced ADR 0005: Token-centric flowchart execution model for improved loop and join handling.
- Added ADR 0006: Tenant Deleted event for distinct handling of tenant removal.
- Documented ADR 0007: Explicit merge modes for flowchart joins, improving reliability and configurability.
- Included ADR 0008: Standardization of empty string as the default tenant ID for consistency and clarity.

* Add unit tests for tenant ID normalization and multitenancy pipeline invoker

- Added comprehensive unit tests for tenant ID normalization to ensure consistent handling of null, empty, and valid IDs.
- Introduced tests for the multitenancy pipeline invoker covering various tenant resolution scenarios.
- Updated solution to include new unit testing projects for `Elsa.Tenants` and `Elsa.Common`.

* Update unit tests for `ActivityConstructionResult`

- Refactor test parameterization to verify `HasExceptions` property more explicitly.
- Simplify exception creation logic in helper methods.
- Improve test assertions by combining act and assert phases where applicable.

* Enable configuration-based multitenancy with tenant-specific settings

- Introduced a configuration-based tenant provider to streamline tenant initialization and customization.
- Added tenant ID handling filters to ensure tenant ID is applied and filtered automatically.
- Deprecated the `CommonPersistenceFeature` in favor of modular persistence feature extension.

* Update database indexes to include `TenantId` for multitenancy support

- Added `TenantId` to unique constraints on `Triggers` table across all EFCore providers.
- Adjusted index names to reflect the updated constraints.
- Updated trigger configuration to ensure uniqueness includes `TenantId`.

* Add tenant filtering to `DefaultWorkflowDefinitionStorePopulator`

- Introduced `ITenantAccessor` to support tenant-specific filtering of workflow definitions.
- Updated logic to skip workflows not matching the current tenant.

* Update doc/adr/toc.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove `CommonPersistenceFeature` as it has been deprecated

* Add tenant-specific filtering to workflow import logic in `DefaultWorkflowDefinitionStorePopulator`

* Replace hardcoded tenant ID with `Tenant.DefaultTenantId` in integration tests

* Update database indexes and migration logic to support `TenantId` for multitenancy

- Added `TenantId` to unique constraints on the `Triggers` table and updated index names.
- Included logic to drop outdated indexes without `TenantId` during migration.
- Adjusted tests to account for `TenantId` in workflow identity and indexing scenarios.

* Remove `TenantId` from workflow identity construction in concurrent trigger indexing tests

* Introduce `SelectiveMockLockProvider` for precise lock mocking in tests

- Added `SelectiveMockLockProvider` to allow targeted lock mocking without affecting unrelated background operations.
- Updated test services to use `SelectiveMockLockProvider` in place of `TestDistributedLockProvider`.
- Refactored `DistributedLockResilienceTests` to support selective mocking for deterministic and reliable assertions.

* Update Elsa.sln

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Normalize tenant ID handling in `DefaultWorkflowDefinitionStorePopulator` for consistent filtering

* Refactor `TenantResolverResult` to support explicit resolved/unresolved state handling

- Updated `TenantResolverResult` to include an explicit `_isResolved` property.
- Adjusted `ResolveTenantId()` and `IsResolved` logic for improved clarity and robustness.
- Simplified tenant resolution invocation in `TenantResolverBase`.
- Removed redundant normalization in `DefaultTenantResolverPipelineInvoker`.

* Normalize tenant ID handling in `DefaultWorkflowDefinitionStorePopulator` and `ClrWorkflowsProvider`.

* Refactor `DefaultWorkflowDefinitionStorePopulatorTests`: streamline object initializations and add tenant-specific test coverage for `PopulateStoreAsync`.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 19:54:13 +01:00

155 lines
3.9 KiB
C#

using Elsa.Common.Multitenancy;
namespace Elsa.Common.UnitTests.Multitenancy;
public class TenantResolverContextTests
{
[Theory]
[InlineData(null, "Default")]
[InlineData("", "Default")]
[InlineData("tenant1", "Tenant 1")]
[InlineData("tenant2", "Tenant 2")]
public void FindTenant_ById_FindsCorrectTenant(string? tenantId, string expectedName)
{
// Arrange
var context = CreateContext();
// Act
var result = context.FindTenant(tenantId!);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedName, result.Name);
}
[Fact]
public void FindTenant_WithNonExistentId_ReturnsNull()
{
// Arrange
var context = CreateContext();
// Act
var result = context.FindTenant("non-existent");
// Assert
Assert.Null(result);
}
[Theory]
[InlineData("Alpha", "tenant1", "Tenant Alpha")]
[InlineData("Beta", "tenant2", "Tenant Beta")]
public void FindTenant_WithPredicate_FindsMatchingTenant(string searchTerm, string expectedId, string expectedName)
{
// Arrange
var context = CreateContextWithNamedTenants();
// Act
var result = context.FindTenant(t => t.Name.Contains(searchTerm));
// Assert
Assert.NotNull(result);
Assert.Equal(expectedId, result.Id);
Assert.Equal(expectedName, result.Name);
}
[Fact]
public void FindTenant_WithPredicate_NoMatch_ReturnsNull()
{
// Arrange
var context = CreateContext();
// Act
var result = context.FindTenant(t => t.Name == "NonExistent");
// Assert
Assert.Null(result);
}
[Fact]
public void Constructor_StoresCancellationToken()
{
// Arrange
using var cts = new CancellationTokenSource();
// Act
var context = new TenantResolverContext(new Dictionary<string, Tenant>(), cts.Token);
// Assert
Assert.Equal(cts.Token, context.CancellationToken);
}
[Fact]
public void FindTenant_NormalizesNullAndEmptyStringToSameValue()
{
// Arrange
var context = CreateContext();
// Act
var resultFromNull = context.FindTenant((string?)null);
var resultFromEmptyString = context.FindTenant(string.Empty);
// Assert
Assert.NotNull(resultFromNull);
Assert.NotNull(resultFromEmptyString);
Assert.Same(resultFromNull, resultFromEmptyString);
}
// Helper methods
private static TenantResolverContext CreateContext()
{
var tenants = new Dictionary<string, Tenant>
{
{
Tenant.DefaultTenantId, new()
{
Id = Tenant.DefaultTenantId,
Name = "Default"
}
},
{
"tenant1", new()
{
Id = "tenant1",
Name = "Tenant 1"
}
},
{
"tenant2", new()
{
Id = "tenant2",
Name = "Tenant 2"
}
}
};
return new(tenants, CancellationToken.None);
}
private static TenantResolverContext CreateContextWithNamedTenants()
{
var tenants = new Dictionary<string, Tenant>
{
{
Tenant.DefaultTenantId, new()
{
Id = Tenant.DefaultTenantId,
Name = "Default"
}
},
{
"tenant1", new()
{
Id = "tenant1",
Name = "Tenant Alpha"
}
},
{
"tenant2", new()
{
Id = "tenant2",
Name = "Tenant Beta"
}
}
};
return new(tenants, CancellationToken.None);
}
}