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
{