diff --git a/all.sln b/all.sln index 1debf1b47..572c8965e 100644 --- a/all.sln +++ b/all.sln @@ -225,6 +225,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers.Test", "test\Dapr.Testcontainers.Test\Dapr.Testcontainers.Test.csproj", "{5A93F96B-4D0E-479D-B540-29678A0998FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.DistributedLock", "test\Dapr.IntegrationTest.DistributedLock\Dapr.IntegrationTest.DistributedLock.csproj", "{E958E875-8DDE-4B25-BE3A-C0760EC89376}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -591,6 +593,10 @@ Global {5A93F96B-4D0E-479D-B540-29678A0998FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A93F96B-4D0E-479D-B540-29678A0998FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A93F96B-4D0E-479D-B540-29678A0998FA}.Release|Any CPU.Build.0 = Release|Any CPU + {E958E875-8DDE-4B25-BE3A-C0760EC89376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E958E875-8DDE-4B25-BE3A-C0760EC89376}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E958E875-8DDE-4B25-BE3A-C0760EC89376}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E958E875-8DDE-4B25-BE3A-C0760EC89376}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -700,6 +706,7 @@ Global {CE5D4439-5B3C-4E97-B7E3-EB8610AEA3EF} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {A05D1519-6A82-498F-B7C9-3D14E08D35CA} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {5A93F96B-4D0E-479D-B540-29678A0998FA} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD} + {E958E875-8DDE-4B25-BE3A-C0760EC89376} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Testcontainers/Containers/LocalStorageCryptographyContainer.cs b/src/Dapr.Testcontainers/Containers/LocalStorageCryptographyContainer.cs index 5e586a92b..b4b9b585e 100644 --- a/src/Dapr.Testcontainers/Containers/LocalStorageCryptographyContainer.cs +++ b/src/Dapr.Testcontainers/Containers/LocalStorageCryptographyContainer.cs @@ -26,25 +26,20 @@ public sealed class LocalStorageCryptographyContainer public static class Yaml { /// - /// Writes a + /// Writes the component yaml. /// - /// - /// - /// - /// - public static string WriteCryptoYamlToFolder(string folderPath, string keyPath, string fileName = "local-crypto.yaml") + public static void WriteCryptoYamlToFolder(string folderPath, string keyPath, string fileName = "local-crypto.yaml") { var yaml = GetLocalStorageYaml(keyPath); - return WriteToFolder(folderPath, fileName, yaml); - } + WriteToFolder(folderPath, fileName, yaml); + } - private static string WriteToFolder(string folderPath, string fileName, string yaml) + private static void WriteToFolder(string folderPath, string fileName, string yaml) { Directory.CreateDirectory(folderPath); var fullPath = Path.Combine(folderPath, fileName); File.WriteAllText(fullPath, yaml); - return fullPath; - } + } private static string GetLocalStorageYaml(string keyPath) => $@"apiVersion: dapr.io/v1alpha diff --git a/src/Dapr.Testcontainers/Containers/OllamaContainer.cs b/src/Dapr.Testcontainers/Containers/OllamaContainer.cs index c2f375a5f..d581c7edd 100644 --- a/src/Dapr.Testcontainers/Containers/OllamaContainer.cs +++ b/src/Dapr.Testcontainers/Containers/OllamaContainer.cs @@ -28,7 +28,7 @@ namespace Dapr.Testcontainers.Containers; public sealed class OllamaContainer : IAsyncStartable { private const int InternalPort = 11434; - private string _containerName = $"ollama-{Guid.NewGuid():N}"; + private readonly string _containerName = $"ollama-{Guid.NewGuid():N}"; private readonly IContainer _container; @@ -83,19 +83,18 @@ public static class Yaml /// /// Writes the component YAML. /// - public static string WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1") + public static void WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1") { var yaml = GetConversationYaml(model, cacheTtl, endpoint); - return WriteToFolder(folderPath, fileName, yaml); - } + WriteToFolder(folderPath, fileName, yaml); + } - private static string WriteToFolder(string folderPath, string fileName, string yaml) + private static void WriteToFolder(string folderPath, string fileName, string yaml) { Directory.CreateDirectory(folderPath); var fullPath = Path.Combine(folderPath, fileName); File.WriteAllText(fullPath, yaml); - return fullPath; - } + } private static string GetConversationYaml(string model, string cacheTtl, string endpoint) => $@"apiVersion: dapr.io/v1alpha diff --git a/src/Dapr.Testcontainers/Containers/RabbitMqContainer.cs b/src/Dapr.Testcontainers/Containers/RabbitMqContainer.cs index 7392a6f10..cdc9f5c84 100644 --- a/src/Dapr.Testcontainers/Containers/RabbitMqContainer.cs +++ b/src/Dapr.Testcontainers/Containers/RabbitMqContainer.cs @@ -85,18 +85,17 @@ public static class Yaml /// /// Writes a PubSub YAML component. /// - public static string WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml", string rabbitmqHost = "localhost:5672") + public static void WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml", string rabbitmqHost = "localhost:5672") { var yaml = GetPubSubYaml(rabbitmqHost); - return WriteToFolder(folderPath, fileName, yaml); - } + WriteToFolder(folderPath, fileName, yaml); + } - private static string WriteToFolder(string folderPath, string fileName, string yaml) + private static void WriteToFolder(string folderPath, string fileName, string yaml) { Directory.CreateDirectory(folderPath); var fullPath = Path.Combine(folderPath, fileName); File.WriteAllText(fullPath, yaml); - return fullPath; } private static string GetPubSubYaml(string rabbitmqHost) => diff --git a/src/Dapr.Testcontainers/Containers/RedisContainer.cs b/src/Dapr.Testcontainers/Containers/RedisContainer.cs index a6f94c27f..363ba253e 100644 --- a/src/Dapr.Testcontainers/Containers/RedisContainer.cs +++ b/src/Dapr.Testcontainers/Containers/RedisContainer.cs @@ -82,29 +82,28 @@ public static class Yaml /// /// Writes a state store YAML component. /// - public static string WriteStateStoreYamlToFolder(string folderPath, string fileName = "redis-state.yaml",string redisHost = "localhost:6379", + public static void WriteStateStoreYamlToFolder(string folderPath, string fileName = "redis-state.yaml",string redisHost = "localhost:6379", string? passwordSecretName = null) { var yaml = GetRedisStateStoreYaml(redisHost, passwordSecretName); - return WriteToFolder(folderPath, fileName, yaml); - } + WriteToFolder(folderPath, fileName, yaml); + } /// /// Writes a distributed lock YAML component. /// - public static string WriteDistributedLockYamlToFolder(string folderPath, string fileName = "redis-lock.yaml", + public static void WriteDistributedLockYamlToFolder(string folderPath, string fileName = "redis-lock.yaml", string redisHost = "localhost:6379", string? passwordSecretName = null) { var yaml = GetDistributedLockYaml(redisHost, passwordSecretName); - return WriteToFolder(folderPath, fileName, yaml); - } + WriteToFolder(folderPath, fileName, yaml); + } - private static string WriteToFolder(string folderPath, string fileName, string yaml) + private static void WriteToFolder(string folderPath, string fileName, string yaml) { Directory.CreateDirectory(folderPath); var fullPath = Path.Combine(folderPath, fileName); File.WriteAllText(fullPath, yaml); - return fullPath; } private static string BuildSecretBlock(string? passwordSecretName) => diff --git a/src/Dapr.Testcontainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.Testcontainers/Harnesses/DistributedLockHarness.cs index 12a51b5c3..ca3435105 100644 --- a/src/Dapr.Testcontainers/Harnesses/DistributedLockHarness.cs +++ b/src/Dapr.Testcontainers/Harnesses/DistributedLockHarness.cs @@ -27,6 +27,11 @@ public sealed class DistributedLockHarness : BaseHarness private readonly RedisContainer _redis; private readonly string componentsDir; + /// + /// The name of the producted distributed lock component. + /// + public const string DistributedLockComponentName = Constants.DaprComponentNames.DistributedLockComponentName; + /// /// Provides an implementation harness for Dapr's distributed lock building block. /// diff --git a/test/Dapr.IntegrationTest.DistributedLock/Dapr.IntegrationTest.DistributedLock.csproj b/test/Dapr.IntegrationTest.DistributedLock/Dapr.IntegrationTest.DistributedLock.csproj new file mode 100644 index 000000000..346632fb0 --- /dev/null +++ b/test/Dapr.IntegrationTest.DistributedLock/Dapr.IntegrationTest.DistributedLock.csproj @@ -0,0 +1,26 @@ + + + + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Dapr.IntegrationTest.DistributedLock/DistributedLockTests.cs b/test/Dapr.IntegrationTest.DistributedLock/DistributedLockTests.cs new file mode 100644 index 000000000..9b3839aa4 --- /dev/null +++ b/test/Dapr.IntegrationTest.DistributedLock/DistributedLockTests.cs @@ -0,0 +1,171 @@ +using Dapr.DistributedLock; +using Dapr.DistributedLock.Extensions; +using Dapr.DistributedLock.Models; +using Dapr.Testcontainers.Common; +using Dapr.Testcontainers.Common.Options; +using Dapr.Testcontainers.Harnesses; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.IntegrationTest.DistributedLock; + +public sealed class DistributedLockTests +{ + [Fact] + public async Task ShouldAcquireAndReleaseLock() + { + var options = new DaprRuntimeOptions(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("distributedlock-components"); + var resourceId = $"resource-{Guid.NewGuid():N}"; + var owner = $"owner-{Guid.NewGuid():N}"; + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(); + await environment.StartAsync(); + + var harness = new DaprHarnessBuilder(options, environment).BuildDistributedLock(componentsDir); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprDistributedLock((sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + const string componentName = DistributedLockHarness.DistributedLockComponentName; + Assert.NotNull(componentName); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + var acquired = await client.TryLockAsync(componentName, resourceId, owner, expiryInSeconds: 10); + Assert.NotNull(acquired); + + var unlock = await client.TryUnlockAsync(componentName, resourceId, owner); + Assert.Equal(LockStatus.Success, unlock.Status); + } + + [Fact] + public async Task ShouldEnforceExclusivityAndReturnExpectedUnlockStatuses() + { + var options = new DaprRuntimeOptions(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("distributedlock-components"); + var resourceId = $"resource-{Guid.NewGuid():N}"; + var owner1 = $"owner-{Guid.NewGuid():N}"; + var owner2 = $"owner-{Guid.NewGuid():N}"; + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(); + await environment.StartAsync(); + + var harness = new DaprHarnessBuilder(options, environment).BuildDistributedLock(componentsDir); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprDistributedLock((sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + const string componentName = DistributedLockHarness.DistributedLockComponentName; + Assert.NotNull(componentName); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + var lock1 = await client.TryLockAsync(componentName, resourceId, owner1, expiryInSeconds: 20); + Assert.NotNull(lock1); + + // While owner1 holds the lock, owner2 should not be able to acquire it. + var lock2 = await client.TryLockAsync(componentName, resourceId, owner2, expiryInSeconds: 20); + Assert.Null(lock2); + + // Wrong owner tries to unlock -> should indicate ownership mismatch. + var wrongUnlock = await client.TryUnlockAsync(componentName, resourceId, owner2); + Assert.Equal(LockStatus.LockBelongsToOthers, wrongUnlock.Status); + + // Correct owner unlocks -> success. + var correctUnlock = await client.TryUnlockAsync(componentName, resourceId, owner1); + Assert.Equal(LockStatus.Success, correctUnlock.Status); + + // Unlocking again after release -> lock does not exist. + var secondUnlock = await client.TryUnlockAsync(componentName, resourceId, owner1); + Assert.Equal(LockStatus.LockDoesNotExist, secondUnlock.Status); + } + + [Fact] + public async Task ShouldAllowAcquireAfterExpiry() + { + var options = new DaprRuntimeOptions(); + var componentsDir = TestDirectoryManager.CreateTestDirectory("distributedlock-components"); + var resourceId = $"resource-{Guid.NewGuid():N}"; + var owner1 = $"owner-{Guid.NewGuid():N}"; + var owner2 = $"owner-{Guid.NewGuid():N}"; + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(); + await environment.StartAsync(); + + var harness = new DaprHarnessBuilder(options, environment).BuildDistributedLock(componentsDir); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprDistributedLock((sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + }); + }) + .BuildAndStartAsync(); + + const string componentName = DistributedLockHarness.DistributedLockComponentName; + Assert.NotNull(componentName); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + // Acquire a short-lived lock and *do not* unlock it. + var first = await client.TryLockAsync(componentName, resourceId, owner1, expiryInSeconds: 2); + Assert.NotNull(first); + + // Poll until the lock becomes available and owner2 can acquire it. + var acquiredByOwner2 = await WaitUntilAsync( + async () => await client.TryLockAsync(componentName, resourceId, owner2, expiryInSeconds: 10), + isSuccess: lr => lr is not null, + timeout: TimeSpan.FromSeconds(30), + pollInterval: TimeSpan.FromMilliseconds(250)); + + Assert.NotNull(acquiredByOwner2); + + var unlock2 = await client.TryUnlockAsync(componentName, resourceId, owner2); + Assert.Equal(LockStatus.Success, unlock2.Status); + } + + private static async Task WaitUntilAsync( + Func> action, + Func isSuccess, + TimeSpan timeout, + TimeSpan pollInterval) + { + using var cts = new CancellationTokenSource(timeout); + while (!cts.IsCancellationRequested) + { + var result = await action(); + if (isSuccess(result)) + return result; + + await Task.Delay(pollInterval, cts.Token); + } + + throw new TimeoutException($"Condition was not met within {timeout}."); + } +} diff --git a/test/Dapr.IntegrationTest.Jobs/Dapr.IntegrationTest.Jobs.csproj b/test/Dapr.IntegrationTest.Jobs/Dapr.IntegrationTest.Jobs.csproj index 1492c219a..a407d3653 100644 --- a/test/Dapr.IntegrationTest.Jobs/Dapr.IntegrationTest.Jobs.csproj +++ b/test/Dapr.IntegrationTest.Jobs/Dapr.IntegrationTest.Jobs.csproj @@ -4,7 +4,6 @@ enable enable false - Dapr.E2E.Test.Jobs diff --git a/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs b/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs index 14426d658..167a0efcd 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs @@ -22,7 +22,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Dapr.E2E.Test.Jobs; +namespace Dapr.IntegrationTest.Jobs; public sealed class JobFailurePolicyTests { diff --git a/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs b/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs index 1ae20e60d..38d041237 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs @@ -21,7 +21,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Dapr.E2E.Test.Jobs; +namespace Dapr.IntegrationTest.Jobs; public sealed class JobManagementTests { diff --git a/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs b/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs index e797cba40..db836170c 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs @@ -22,9 +22,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Xunit.Sdk; -namespace Dapr.E2E.Test.Jobs; +namespace Dapr.IntegrationTest.Jobs; public sealed class JobPayloadTests { diff --git a/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs b/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs index d8ed64ce0..75d7f955c 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs @@ -21,7 +21,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Dapr.E2E.Test.Jobs; +namespace Dapr.IntegrationTest.Jobs; public sealed class JobSchedulingTests { diff --git a/test/Dapr.IntegrationTest.Jobs/JobsTests.cs b/test/Dapr.IntegrationTest.Jobs/JobsTests.cs index 5bf5e417b..5d8edab03 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobsTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobsTests.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.ComponentModel.DataAnnotations; using System.Text; using Dapr.Jobs; using Dapr.Jobs.Extensions; @@ -23,7 +22,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Dapr.E2E.Test.Jobs; +namespace Dapr.IntegrationTest.Jobs; public sealed class JobsTests {