diff --git a/Elsa.sln b/Elsa.sln index 70b5c24e5..02e499f8f 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -525,6 +525,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Workflows.Persistence.ConformanceTests", "test\integration\Elsa.Workflows.Persistence.ConformanceTests\Elsa.Workflows.Persistence.ConformanceTests.csproj", "{E099B352-C3E6-499D-B141-67B209125B67}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Labels.Persistence.ConformanceTests", "test\integration\Elsa.Labels.Persistence.ConformanceTests\Elsa.Labels.Persistence.ConformanceTests.csproj", "{7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2513,6 +2515,18 @@ Global {E099B352-C3E6-499D-B141-67B209125B67}.Release|x64.Build.0 = Release|Any CPU {E099B352-C3E6-499D-B141-67B209125B67}.Release|x86.ActiveCfg = Release|Any CPU {E099B352-C3E6-499D-B141-67B209125B67}.Release|x86.Build.0 = Release|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Debug|x64.Build.0 = Debug|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Debug|x86.Build.0 = Debug|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Release|Any CPU.Build.0 = Release|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Release|x64.ActiveCfg = Release|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Release|x64.Build.0 = Release|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Release|x86.ActiveCfg = Release|Any CPU + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2721,6 +2735,7 @@ Global {ED8D7C78-154D-4124-8C0A-E63785A862E5} = {18453B51-25EB-4317-A4B3-B10518252E92} {EAF427BE-7A07-42D7-A223-70FB1FC91BB2} = {18453B51-25EB-4317-A4B3-B10518252E92} {E099B352-C3E6-499D-B141-67B209125B67} = {823D4020-332D-2C13-F261-6F510F11A57E} + {7C4E1A92-6B8F-4D3A-9E11-2F5A8C0B4D17} = {823D4020-332D-2C13-F261-6F510F11A57E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/test/integration/Elsa.Labels.Persistence.ConformanceTests/Elsa.Labels.Persistence.ConformanceTests.csproj b/test/integration/Elsa.Labels.Persistence.ConformanceTests/Elsa.Labels.Persistence.ConformanceTests.csproj new file mode 100644 index 000000000..bb5e93000 --- /dev/null +++ b/test/integration/Elsa.Labels.Persistence.ConformanceTests/Elsa.Labels.Persistence.ConformanceTests.csproj @@ -0,0 +1,19 @@ + + + + [Elsa.Labels]*,[Elsa.Persistence.EFCore]* + 0 + + + + + + + + + + + + + + diff --git a/test/integration/Elsa.Labels.Persistence.ConformanceTests/LabelStoreConformanceTests.cs b/test/integration/Elsa.Labels.Persistence.ConformanceTests/LabelStoreConformanceTests.cs new file mode 100644 index 000000000..a5207eb1e --- /dev/null +++ b/test/integration/Elsa.Labels.Persistence.ConformanceTests/LabelStoreConformanceTests.cs @@ -0,0 +1,382 @@ +using Elsa.Common.Models; +using Elsa.Common.Multitenancy; +using Elsa.Labels.Entities; + +namespace Elsa.Labels.Persistence.ConformanceTests; + +/// +/// Shared InMemory / EF Core store-contract assertions for Labels ports. +/// +public abstract class LabelStoreConformanceTests +{ + protected abstract Task CreateScenarioAsync(); + + [Fact] + public async Task DeletingALabelCascadesItsAssociations() + { + await using var scenario = await CreateScenarioAsync(); + await scenario.Labels.SaveAsync(Label("label-a", "A", "tenant-a")); + await scenario.Labels.SaveAsync(Label("label-keep", "Keep", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-a1", "label-a", "order", "order:1", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-a2", "label-a", "invoice", "invoice:1", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-keep", "label-keep", "order", "order:1", "tenant-a")); + + using (scenario.UseTenant("tenant-b")) + { + await scenario.Labels.SaveAsync(Label("label-b", "A", "tenant-b")); + await scenario.Associations.SaveAsync(Association("assoc-b", "label-b", "order", "order:1", "tenant-b")); + } + + Assert.True(await scenario.Labels.DeleteAsync("label-a")); + Assert.Empty(await scenario.AssociationQuery.FindByLabelIdsAsync(["label-a"])); + Assert.Equal("assoc-keep", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["label-keep"])).Id); + using (scenario.UseTenant("tenant-b")) + { + Assert.NotNull(await scenario.Labels.FindByIdAsync("label-b")); + Assert.Equal("assoc-b", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["label-b"])).Id); + } + + await scenario.Labels.SaveAsync(Label("label-x", "X", "tenant-a")); + await scenario.Labels.SaveAsync(Label("label-y", "Y", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-x", "label-x", "order", "order:2", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-y", "label-y", "order", "order:2", "tenant-a")); + + Assert.Equal(2, await scenario.Labels.DeleteManyAsync(["label-x", "label-y"])); + Assert.Empty(await scenario.AssociationQuery.FindByLabelIdsAsync(["label-x", "label-y"])); + Assert.Equal("assoc-keep", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["label-keep"])).Id); + } + + [Fact] + public async Task ReplaceAsyncRemovesAndAddsByAssociationId() + { + await using var scenario = await CreateScenarioAsync(); + await scenario.Associations.SaveAsync(Association("assoc-red", "red", "order", "order:1", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-blue", "blue", "order", "order:1", "tenant-a")); + + await scenario.Associations.ReplaceAsync( + [Association("assoc-red", "wrong-label", "other", "other:9", "tenant-b")], + [Association("assoc-green", "green", "order", "order:1", "tenant-a")]); + + var remaining = (await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")) + .Select(x => x.Id) + .OrderBy(x => x) + .ToList(); + Assert.Equal(["assoc-blue", "assoc-green"], remaining); + } + + [Fact] + public async Task AssociationFindsAndDeletesHonorWorkflowDefinitionKeys() + { + await using var scenario = await CreateScenarioAsync(); + await scenario.Associations.SaveAsync(Association("assoc-v1-red", "red", "order", "order:1", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-v1-blue", "blue", "order", "order:1", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-v2-red", "red", "order", "order:2", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-invoice", "red", "invoice", "invoice:1", "tenant-a")); + + var version1 = (await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")).ToList(); + Assert.Equal(2, version1.Count); + Assert.Contains(version1, x => x.Id == "assoc-v1-red"); + Assert.Contains(version1, x => x.Id == "assoc-v1-blue"); + Assert.Empty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("missing")); + + Assert.Empty(await scenario.AssociationQuery.FindByLabelIdsAsync([])); + Assert.Empty(await scenario.AssociationQuery.FindByLabelIdsAsync(["unknown"])); + var byRed = (await scenario.AssociationQuery.FindByLabelIdsAsync(["red"])).ToList(); + Assert.Equal(3, byRed.Count); + Assert.Contains(byRed, x => x.Id == "assoc-v1-red"); + Assert.Contains(byRed, x => x.Id == "assoc-v2-red"); + Assert.Contains(byRed, x => x.Id == "assoc-invoice"); + + var byRedAndBlue = (await scenario.AssociationQuery.FindByLabelIdsAsync(["red", "blue"])).ToList(); + Assert.Equal(4, byRedAndBlue.Count); + Assert.Contains(byRedAndBlue, x => x.Id == "assoc-v1-blue"); + + Assert.Equal(1, await scenario.Associations.DeleteByWorkflowDefinitionVersionIdAsync("order:2")); + Assert.Empty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:2")); + Assert.NotEmpty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")); + + Assert.Equal(1, await scenario.Associations.DeleteByWorkflowDefinitionIdAsync("invoice")); + Assert.Empty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("invoice:1")); + + await scenario.Associations.SaveManyAsync( + [ + Association("assoc-pay-1", "red", "payroll", "payroll:1", "tenant-a"), + Association("assoc-pay-2", "red", "payroll", "payroll:2", "tenant-a"), + Association("assoc-hr-1", "blue", "hr", "hr:1", "tenant-a") + ]); + Assert.Equal(2, await scenario.Associations.DeleteByWorkflowDefinitionVersionIdsAsync(["payroll:1", "payroll:2"])); + Assert.Equal(1, await scenario.Associations.DeleteByWorkflowDefinitionIdsAsync(["hr"])); + Assert.Empty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("payroll:1")); + Assert.Empty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("hr:1")); + Assert.NotEmpty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")); + } + + [Fact] + public async Task ListAsyncOrdersByNameAndPages() + { + await using var scenario = await CreateScenarioAsync(); + await scenario.Labels.SaveAsync(Label("label-z", "Zebra", "tenant-a")); + await scenario.Labels.SaveAsync(Label("label-a", "Apple", "tenant-a")); + await scenario.Labels.SaveAsync(Label("label-m", "Mango", "tenant-a")); + + var all = (await scenario.Labels.ListAsync()).Items.Select(x => x.Name).ToList(); + Assert.Equal(["Apple", "Mango", "Zebra"], all); + + // Page.TotalCount is not a shared contract: Memory ToPage counts after Skip/Take, + // EF PaginateAsync counts the unpaged query. + var firstPage = await scenario.Labels.ListAsync(PageArgs.FromRange(0, 2)); + Assert.Equal(["Apple", "Mango"], firstPage.Items.Select(x => x.Name).ToList()); + + var secondPage = await scenario.Labels.ListAsync(PageArgs.FromRange(2, 2)); + Assert.Equal(["Zebra"], secondPage.Items.Select(x => x.Name).ToList()); + } + + [Fact] + public async Task TenantStampAndIsolationHonorAmbientTenant() + { + await using var scenario = await CreateScenarioAsync(); + await SeedMixedLabelsAsync(scenario); + await SeedMixedAssociationsAsync(scenario); + + var listed = (await scenario.Labels.ListAsync()).Items.ToList(); + Assert.Equal(2, listed.Count); + Assert.Contains(listed, x => x.Id == "label-a"); + Assert.Contains(listed, x => x.Id == "label-star"); + Assert.DoesNotContain(listed, x => x.Id == "label-b"); + + Assert.Null(await scenario.Labels.FindByIdAsync("label-b")); + Assert.NotNull(await scenario.Labels.FindByIdAsync("label-a")); + Assert.NotNull(await scenario.Labels.FindByIdAsync("label-star")); + + var foundMany = (await scenario.Labels.FindManyByIdAsync(["label-a", "label-b", "label-star"])).ToList(); + Assert.Equal(2, foundMany.Count); + Assert.DoesNotContain(foundMany, x => x.Id == "label-b"); + + var byVersion = (await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")).ToList(); + Assert.Equal(2, byVersion.Count); + Assert.Contains(byVersion, x => x.Id == "assoc-a"); + Assert.Contains(byVersion, x => x.Id == "assoc-star"); + Assert.DoesNotContain(byVersion, x => x.Id == "assoc-b"); + + var byLabel = (await scenario.AssociationQuery.FindByLabelIdsAsync(["red"])).ToList(); + Assert.Equal(2, byLabel.Count); + Assert.DoesNotContain(byLabel, x => x.Id == "assoc-b"); + + using (scenario.UseTenant("tenant-b")) + { + var tenantBLabels = (await scenario.Labels.ListAsync()).Items.ToList(); + Assert.Contains(tenantBLabels, x => x.Id == "label-star"); + Assert.DoesNotContain(tenantBLabels, x => x.Id == "label-a"); + Assert.Equal("label-star", (await scenario.Labels.FindByIdAsync("label-star"))!.Id); + + var tenantBByVersion = (await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")).ToList(); + Assert.Equal(2, tenantBByVersion.Count); + Assert.Contains(tenantBByVersion, x => x.Id == "assoc-b"); + Assert.Contains(tenantBByVersion, x => x.Id == "assoc-star"); + Assert.DoesNotContain(tenantBByVersion, x => x.Id == "assoc-a"); + + var tenantBByLabel = (await scenario.AssociationQuery.FindByLabelIdsAsync(["red"])).ToList(); + Assert.Equal(2, tenantBByLabel.Count); + Assert.Contains(tenantBByLabel, x => x.Id == "assoc-b"); + Assert.Contains(tenantBByLabel, x => x.Id == "assoc-star"); + Assert.DoesNotContain(tenantBByLabel, x => x.Id == "assoc-a"); + } + + Assert.False(await scenario.Labels.DeleteAsync("label-b")); + Assert.False(await scenario.Associations.DeleteAsync("assoc-b")); + using (scenario.UseTenant("tenant-b")) + { + Assert.NotNull(await scenario.Labels.FindByIdAsync("label-b")); + Assert.Contains(await scenario.AssociationQuery.FindByLabelIdsAsync(["red"]), x => x.Id == "assoc-b"); + } + + await scenario.Associations.ReplaceAsync( + [Association("assoc-a", "red", "order", "order:1", "tenant-a"), Association("assoc-b", "red", "order", "order:1", "tenant-b")], + [Association("assoc-a2", "red", "order", "order:1", "tenant-a")]); + var remainingA = (await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")).ToList(); + Assert.Contains(remainingA, x => x.Id == "assoc-a2"); + Assert.Contains(remainingA, x => x.Id == "assoc-star"); + Assert.DoesNotContain(remainingA, x => x.Id == "assoc-a"); + using (scenario.UseTenant("tenant-b")) + Assert.Contains(await scenario.AssociationQuery.FindByLabelIdsAsync(["red"]), x => x.Id == "assoc-b"); + + var deletedLabels = await scenario.Labels.DeleteManyAsync(["label-a", "label-b", "label-star"]); + Assert.Equal(2, deletedLabels); + using (scenario.UseTenant("tenant-b")) + Assert.Equal("label-b", (await scenario.Labels.FindByIdAsync("label-b"))!.Id); + + using (scenario.UseTenant("tenant-b")) + await scenario.Associations.SaveAsync(Association("assoc-b-order", "blue", "order", "order:1", "tenant-b")); + Assert.Equal(2, await scenario.Associations.DeleteByWorkflowDefinitionIdAsync("order")); + Assert.Empty(await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")); + using (scenario.UseTenant("tenant-b")) + { + var stillB = (await scenario.Associations.FindByWorkflowDefinitionVersionIdAsync("order:1")).ToList(); + Assert.Contains(stillB, x => x.Id == "assoc-b"); + Assert.Contains(stillB, x => x.Id == "assoc-b-order"); + } + + using (scenario.UseTenant(Tenant.DefaultTenantId)) + { + var stampedLabel = Label("label-null", "Null", tenantId: null); + await scenario.Labels.SaveAsync(stampedLabel); + Assert.Equal(Tenant.DefaultTenantId, stampedLabel.TenantId); + + var namedLabel = Label("label-named", "Named", "tenant-a"); + await scenario.Labels.SaveAsync(namedLabel); + Assert.Equal("tenant-a", namedLabel.TenantId); + + var defaultLabels = (await scenario.Labels.ListAsync()).Items.ToList(); + Assert.Contains(defaultLabels, x => x.Id == "label-null"); + Assert.DoesNotContain(defaultLabels, x => x.Id == "label-named"); + + var stampedAssociation = Association("assoc-null", "red", "order", "order:1", tenantId: null); + await scenario.Associations.SaveAsync(stampedAssociation); + Assert.Equal(Tenant.DefaultTenantId, stampedAssociation.TenantId); + Assert.Equal("assoc-null", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["red"])).Id); + + var batchLabels = new[] { Label("label-batch-null", "BatchNull", tenantId: null) }; + await scenario.Labels.SaveManyAsync(batchLabels); + Assert.Equal(Tenant.DefaultTenantId, batchLabels[0].TenantId); + Assert.Equal(Tenant.DefaultTenantId, (await scenario.Labels.FindByIdAsync("label-batch-null"))!.TenantId); + + var batchAssociations = new[] { Association("assoc-batch-null", "blue", "order", "order:1", tenantId: null) }; + await scenario.Associations.SaveManyAsync(batchAssociations); + Assert.Equal(Tenant.DefaultTenantId, batchAssociations[0].TenantId); + Assert.Equal("assoc-batch-null", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["blue"])).Id); + } + + var agnostic = Label("label-star-2", "Star2", Tenant.AgnosticTenantId); + await scenario.Labels.SaveAsync(agnostic); + Assert.Equal(Tenant.AgnosticTenantId, agnostic.TenantId); + + var stampedNamedLabel = Label("label-named-stamp", "NamedStamp", tenantId: null); + await scenario.Labels.SaveAsync(stampedNamedLabel); + Assert.Equal("tenant-a", stampedNamedLabel.TenantId); + Assert.Equal("tenant-a", (await scenario.Labels.FindByIdAsync("label-named-stamp"))!.TenantId); + + var stampedNamedLabelBatch = new[] { Label("label-named-batch", "NamedBatch", tenantId: null) }; + await scenario.Labels.SaveManyAsync(stampedNamedLabelBatch); + Assert.Equal("tenant-a", stampedNamedLabelBatch[0].TenantId); + Assert.Equal("tenant-a", (await scenario.Labels.FindByIdAsync("label-named-batch"))!.TenantId); + + var stampedNamedAssociation = Association("assoc-named-stamp", "orange", "order", "order:1", tenantId: null); + await scenario.Associations.SaveAsync(stampedNamedAssociation); + Assert.Equal("tenant-a", stampedNamedAssociation.TenantId); + Assert.Equal("assoc-named-stamp", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["orange"])).Id); + + var stampedNamedAssociationBatch = new[] { Association("assoc-named-batch", "purple", "order", "order:1", tenantId: null) }; + await scenario.Associations.SaveManyAsync(stampedNamedAssociationBatch); + Assert.Equal("tenant-a", stampedNamedAssociationBatch[0].TenantId); + Assert.Equal("assoc-named-batch", Assert.Single(await scenario.AssociationQuery.FindByLabelIdsAsync(["purple"])).Id); + } + + [Fact] + public async Task NormalizedNamesAreUniquePerTenant() + { + await using var scenario = await CreateScenarioAsync(); + + await scenario.Labels.SaveAsync(Label("label-1", "Urgent", "tenant-a")); + await scenario.AssertUniquenessConflictAsync(() => scenario.Labels.SaveAsync(Label("label-2", "urgent", "tenant-a"))); + Assert.Equal("label-1", Assert.Single((await scenario.Labels.ListAsync()).Items).Id); + + using (scenario.UseTenant("tenant-b")) + await scenario.Labels.SaveAsync(Label("label-b", "Urgent", "tenant-b")); + Assert.Equal("urgent", (await scenario.Labels.FindByIdAsync("label-1"))!.NormalizedName); + using (scenario.UseTenant("tenant-b")) + Assert.Equal("urgent", (await scenario.Labels.FindByIdAsync("label-b"))!.NormalizedName); + + await scenario.Labels.SaveAsync(Label("label-1", "Critical", "tenant-a")); + var updated = await scenario.Labels.FindByIdAsync("label-1"); + Assert.Equal("Critical", updated!.Name); + Assert.Equal("critical", updated.NormalizedName); + + await scenario.Labels.SaveAsync(Label("label-later", "Later", "tenant-a")); + await scenario.AssertUniquenessConflictAsync(() => scenario.Labels.SaveAsync(Label("label-later", "Critical", "tenant-a"))); + Assert.Equal("Later", (await scenario.Labels.FindByIdAsync("label-later"))!.Name); + + await scenario.Labels.SaveAsync(Label("label-star", "Critical", Tenant.AgnosticTenantId)); + Assert.NotNull(await scenario.Labels.FindByIdAsync("label-star")); + + using (scenario.UseTenant(Tenant.DefaultTenantId)) + { + await scenario.Labels.SaveAsync(Label("label-default", "Shared", tenantId: null)); + await scenario.AssertUniquenessConflictAsync(() => scenario.Labels.SaveAsync(Label("label-default-dup", "Shared", tenantId: null))); + Assert.Equal(Tenant.DefaultTenantId, (await scenario.Labels.FindByIdAsync("label-default"))!.TenantId); + } + + await scenario.AssertUniquenessConflictAsync(() => + scenario.Labels.SaveManyAsync([Label("label-3", "Later", "tenant-a")])); + Assert.Null(await scenario.Labels.FindByIdAsync("label-3")); + + await scenario.AssertUniquenessConflictAsync(() => scenario.Labels.SaveManyAsync( + [ + Label("label-batch-1", "Invoice", "tenant-a"), + Label("label-batch-2", "invoice", "tenant-a") + ])); + Assert.Null(await scenario.Labels.FindByIdAsync("label-batch-1")); + Assert.Null(await scenario.Labels.FindByIdAsync("label-batch-2")); + Assert.Equal("Later", (await scenario.Labels.FindByIdAsync("label-later"))!.Name); + } + + private static async Task SeedMixedLabelsAsync(LabelStoreScenario scenario) + { + await scenario.Labels.SaveAsync(Label("label-a", "A", "tenant-a")); + await scenario.Labels.SaveAsync(Label("label-b", "B", "tenant-b")); + await scenario.Labels.SaveAsync(Label("label-star", "Star", Tenant.AgnosticTenantId)); + } + + private static async Task SeedMixedAssociationsAsync(LabelStoreScenario scenario) + { + await scenario.Associations.SaveAsync(Association("assoc-a", "red", "order", "order:1", "tenant-a")); + await scenario.Associations.SaveAsync(Association("assoc-b", "red", "order", "order:1", "tenant-b")); + await scenario.Associations.SaveAsync(Association("assoc-star", "red", "order", "order:1", Tenant.AgnosticTenantId)); + } + + private static Label Label(string id, string name, string? tenantId) => + new() + { + Id = id, + Name = name, + TenantId = tenantId + }; + + private static WorkflowDefinitionLabel Association( + string id, + string labelId, + string workflowDefinitionId, + string workflowDefinitionVersionId, + string? tenantId) => + new() + { + Id = id, + LabelId = labelId, + WorkflowDefinitionId = workflowDefinitionId, + WorkflowDefinitionVersionId = workflowDefinitionVersionId, + TenantId = tenantId + }; +} + +[CollectionDefinition(Name)] +public sealed class LabelStoreInMemoryConformanceCollection +{ + public const string Name = "LabelStores:InMemory"; +} + +[CollectionDefinition(Name)] +public sealed class LabelStoreSqliteConformanceCollection +{ + public const string Name = "LabelStores:EFCore.Sqlite"; +} + +[Collection(LabelStoreInMemoryConformanceCollection.Name)] +public sealed class InMemoryLabelStoreConformanceTests : LabelStoreConformanceTests +{ + protected override Task CreateScenarioAsync() => LabelStoreScenario.CreateInMemoryAsync(); +} + +[Collection(LabelStoreSqliteConformanceCollection.Name)] +public sealed class SqliteLabelStoreConformanceTests : LabelStoreConformanceTests +{ + protected override Task CreateScenarioAsync() => LabelStoreScenario.CreateSqliteAsync(); +} diff --git a/test/integration/Elsa.Labels.Persistence.ConformanceTests/LabelStoreScenario.cs b/test/integration/Elsa.Labels.Persistence.ConformanceTests/LabelStoreScenario.cs new file mode 100644 index 000000000..c8b0163ed --- /dev/null +++ b/test/integration/Elsa.Labels.Persistence.ConformanceTests/LabelStoreScenario.cs @@ -0,0 +1,132 @@ +using Elsa.Common.Multitenancy; +using Elsa.Common.Services; +using Elsa.Labels.Contracts; +using Elsa.Labels.Entities; +using Elsa.Labels.Services; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.EntityHandlers; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Persistence.EFCore.Modules.Labels; +using Elsa.Persistence.EFCore.Sqlite; +using Elsa.Tenants.Options; +using Elsa.Testing.Shared.Multitenancy; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Labels.Persistence.ConformanceTests; + +/// +/// Holds one InMemory or EF/SQLite pair of Labels stores and the ambient tenant they read. +/// +public sealed class LabelStoreScenario( + TestTenantAccessor tenantAccessor, + ILabelStore labels, + IWorkflowDefinitionLabelStore associations, + IWorkflowDefinitionLabelQuery associationQuery, + Func, Task> assertUniquenessConflictAsync, + Func disposeAsync) : IAsyncDisposable +{ + public TestTenantAccessor TenantAccessor { get; } = tenantAccessor; + public ILabelStore Labels { get; } = labels; + public IWorkflowDefinitionLabelStore Associations { get; } = associations; + public IWorkflowDefinitionLabelQuery AssociationQuery { get; } = associationQuery; + + public IDisposable UseTenant(string tenantId) => + TenantAccessor.PushContext(tenantId == Tenant.DefaultTenantId + ? Tenant.Default + : new Tenant { Id = tenantId, Name = tenantId }); + + public Task AssertUniquenessConflictAsync(Func operation) => assertUniquenessConflictAsync(operation); + + public ValueTask DisposeAsync() => disposeAsync(); + + public static Task CreateInMemoryAsync() + { + var tenantAccessor = new TestTenantAccessor("tenant-a"); + var labels = new MemoryStore