Skip to content

Commit bde308d

Browse files
authored
Adding integration testing for Dapr.DistributedLock package (#1682)
* Removed RootNamespace value in Jobs e2e project * Fixed namespace since removing the root namespace * Added an e2e project for validating the Dapr.DistributedLock project * Adding e2e tests for the Dapr.DistributedLock project --------- Signed-off-by: Whit Waldo <[email protected]>
1 parent b7f40bc commit bde308d

File tree

14 files changed

+237
-39
lines changed

14 files changed

+237
-39
lines changed

all.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers", "src\
225225
EndProject
226226
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers.Test", "test\Dapr.Testcontainers.Test\Dapr.Testcontainers.Test.csproj", "{5A93F96B-4D0E-479D-B540-29678A0998FA}"
227227
EndProject
228+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.DistributedLock", "test\Dapr.IntegrationTest.DistributedLock\Dapr.IntegrationTest.DistributedLock.csproj", "{E958E875-8DDE-4B25-BE3A-C0760EC89376}"
229+
EndProject
228230
Global
229231
GlobalSection(SolutionConfigurationPlatforms) = preSolution
230232
Debug|Any CPU = Debug|Any CPU
@@ -591,6 +593,10 @@ Global
591593
{5A93F96B-4D0E-479D-B540-29678A0998FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
592594
{5A93F96B-4D0E-479D-B540-29678A0998FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
593595
{5A93F96B-4D0E-479D-B540-29678A0998FA}.Release|Any CPU.Build.0 = Release|Any CPU
596+
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
597+
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Debug|Any CPU.Build.0 = Debug|Any CPU
598+
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Release|Any CPU.ActiveCfg = Release|Any CPU
599+
{E958E875-8DDE-4B25-BE3A-C0760EC89376}.Release|Any CPU.Build.0 = Release|Any CPU
594600
EndGlobalSection
595601
GlobalSection(SolutionProperties) = preSolution
596602
HideSolutionNode = FALSE
@@ -700,6 +706,7 @@ Global
700706
{CE5D4439-5B3C-4E97-B7E3-EB8610AEA3EF} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
701707
{A05D1519-6A82-498F-B7C9-3D14E08D35CA} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
702708
{5A93F96B-4D0E-479D-B540-29678A0998FA} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD}
709+
{E958E875-8DDE-4B25-BE3A-C0760EC89376} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
703710
EndGlobalSection
704711
GlobalSection(ExtensibilityGlobals) = postSolution
705712
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}

src/Dapr.Testcontainers/Containers/LocalStorageCryptographyContainer.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,20 @@ public sealed class LocalStorageCryptographyContainer
2626
public static class Yaml
2727
{
2828
/// <summary>
29-
/// Writes a
29+
/// Writes the component yaml.
3030
/// </summary>
31-
/// <param name="folderPath"></param>
32-
/// <param name="keyPath"></param>
33-
/// <param name="fileName"></param>
34-
/// <returns></returns>
35-
public static string WriteCryptoYamlToFolder(string folderPath, string keyPath, string fileName = "local-crypto.yaml")
31+
public static void WriteCryptoYamlToFolder(string folderPath, string keyPath, string fileName = "local-crypto.yaml")
3632
{
3733
var yaml = GetLocalStorageYaml(keyPath);
38-
return WriteToFolder(folderPath, fileName, yaml);
39-
}
34+
WriteToFolder(folderPath, fileName, yaml);
35+
}
4036

41-
private static string WriteToFolder(string folderPath, string fileName, string yaml)
37+
private static void WriteToFolder(string folderPath, string fileName, string yaml)
4238
{
4339
Directory.CreateDirectory(folderPath);
4440
var fullPath = Path.Combine(folderPath, fileName);
4541
File.WriteAllText(fullPath, yaml);
46-
return fullPath;
47-
}
42+
}
4843

4944
private static string GetLocalStorageYaml(string keyPath) =>
5045
$@"apiVersion: dapr.io/v1alpha

src/Dapr.Testcontainers/Containers/OllamaContainer.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ namespace Dapr.Testcontainers.Containers;
2828
public sealed class OllamaContainer : IAsyncStartable
2929
{
3030
private const int InternalPort = 11434;
31-
private string _containerName = $"ollama-{Guid.NewGuid():N}";
31+
private readonly string _containerName = $"ollama-{Guid.NewGuid():N}";
3232

3333
private readonly IContainer _container;
3434

@@ -83,19 +83,18 @@ public static class Yaml
8383
/// <summary>
8484
/// Writes the component YAML.
8585
/// </summary>
86-
public static string WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1")
86+
public static void WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1")
8787
{
8888
var yaml = GetConversationYaml(model, cacheTtl, endpoint);
89-
return WriteToFolder(folderPath, fileName, yaml);
90-
}
89+
WriteToFolder(folderPath, fileName, yaml);
90+
}
9191

92-
private static string WriteToFolder(string folderPath, string fileName, string yaml)
92+
private static void WriteToFolder(string folderPath, string fileName, string yaml)
9393
{
9494
Directory.CreateDirectory(folderPath);
9595
var fullPath = Path.Combine(folderPath, fileName);
9696
File.WriteAllText(fullPath, yaml);
97-
return fullPath;
98-
}
97+
}
9998

10099
private static string GetConversationYaml(string model, string cacheTtl, string endpoint) =>
101100
$@"apiVersion: dapr.io/v1alpha

src/Dapr.Testcontainers/Containers/RabbitMqContainer.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,17 @@ public static class Yaml
8585
/// <summary>
8686
/// Writes a PubSub YAML component.
8787
/// </summary>
88-
public static string WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml", string rabbitmqHost = "localhost:5672")
88+
public static void WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml", string rabbitmqHost = "localhost:5672")
8989
{
9090
var yaml = GetPubSubYaml(rabbitmqHost);
91-
return WriteToFolder(folderPath, fileName, yaml);
92-
}
91+
WriteToFolder(folderPath, fileName, yaml);
92+
}
9393

94-
private static string WriteToFolder(string folderPath, string fileName, string yaml)
94+
private static void WriteToFolder(string folderPath, string fileName, string yaml)
9595
{
9696
Directory.CreateDirectory(folderPath);
9797
var fullPath = Path.Combine(folderPath, fileName);
9898
File.WriteAllText(fullPath, yaml);
99-
return fullPath;
10099
}
101100

102101
private static string GetPubSubYaml(string rabbitmqHost) =>

src/Dapr.Testcontainers/Containers/RedisContainer.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,29 +82,28 @@ public static class Yaml
8282
/// <summary>
8383
/// Writes a state store YAML component.
8484
/// </summary>
85-
public static string WriteStateStoreYamlToFolder(string folderPath, string fileName = "redis-state.yaml",string redisHost = "localhost:6379",
85+
public static void WriteStateStoreYamlToFolder(string folderPath, string fileName = "redis-state.yaml",string redisHost = "localhost:6379",
8686
string? passwordSecretName = null)
8787
{
8888
var yaml = GetRedisStateStoreYaml(redisHost, passwordSecretName);
89-
return WriteToFolder(folderPath, fileName, yaml);
90-
}
89+
WriteToFolder(folderPath, fileName, yaml);
90+
}
9191

9292
/// <summary>
9393
/// Writes a distributed lock YAML component.
9494
/// </summary>
95-
public static string WriteDistributedLockYamlToFolder(string folderPath, string fileName = "redis-lock.yaml",
95+
public static void WriteDistributedLockYamlToFolder(string folderPath, string fileName = "redis-lock.yaml",
9696
string redisHost = "localhost:6379", string? passwordSecretName = null)
9797
{
9898
var yaml = GetDistributedLockYaml(redisHost, passwordSecretName);
99-
return WriteToFolder(folderPath, fileName, yaml);
100-
}
99+
WriteToFolder(folderPath, fileName, yaml);
100+
}
101101

102-
private static string WriteToFolder(string folderPath, string fileName, string yaml)
102+
private static void WriteToFolder(string folderPath, string fileName, string yaml)
103103
{
104104
Directory.CreateDirectory(folderPath);
105105
var fullPath = Path.Combine(folderPath, fileName);
106106
File.WriteAllText(fullPath, yaml);
107-
return fullPath;
108107
}
109108

110109
private static string BuildSecretBlock(string? passwordSecretName) =>

src/Dapr.Testcontainers/Harnesses/DistributedLockHarness.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public sealed class DistributedLockHarness : BaseHarness
2727
private readonly RedisContainer _redis;
2828
private readonly string componentsDir;
2929

30+
/// <summary>
31+
/// The name of the producted distributed lock component.
32+
/// </summary>
33+
public const string DistributedLockComponentName = Constants.DaprComponentNames.DistributedLockComponentName;
34+
3035
/// <summary>
3136
/// Provides an implementation harness for Dapr's distributed lock building block.
3237
/// </summary>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="coverlet.collector" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
12+
<PackageReference Include="xunit" />
13+
<PackageReference Include="xunit.extensibility.core" />
14+
<PackageReference Include="xunit.runner.visualstudio" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<Using Include="Xunit"/>
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\..\src\Dapr.DistributedLock\Dapr.DistributedLock.csproj" />
23+
<ProjectReference Include="..\..\src\Dapr.Testcontainers\Dapr.Testcontainers.csproj" />
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
using Dapr.DistributedLock;
2+
using Dapr.DistributedLock.Extensions;
3+
using Dapr.DistributedLock.Models;
4+
using Dapr.Testcontainers.Common;
5+
using Dapr.Testcontainers.Common.Options;
6+
using Dapr.Testcontainers.Harnesses;
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
namespace Dapr.IntegrationTest.DistributedLock;
11+
12+
public sealed class DistributedLockTests
13+
{
14+
[Fact]
15+
public async Task ShouldAcquireAndReleaseLock()
16+
{
17+
var options = new DaprRuntimeOptions();
18+
var componentsDir = TestDirectoryManager.CreateTestDirectory("distributedlock-components");
19+
var resourceId = $"resource-{Guid.NewGuid():N}";
20+
var owner = $"owner-{Guid.NewGuid():N}";
21+
22+
await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync();
23+
await environment.StartAsync();
24+
25+
var harness = new DaprHarnessBuilder(options, environment).BuildDistributedLock(componentsDir);
26+
await using var testApp = await DaprHarnessBuilder.ForHarness(harness)
27+
.ConfigureServices(builder =>
28+
{
29+
builder.Services.AddDaprDistributedLock((sp, clientBuilder) =>
30+
{
31+
var config = sp.GetRequiredService<IConfiguration>();
32+
var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"];
33+
if (!string.IsNullOrEmpty(grpcEndpoint))
34+
clientBuilder.UseGrpcEndpoint(grpcEndpoint);
35+
});
36+
})
37+
.BuildAndStartAsync();
38+
39+
const string componentName = DistributedLockHarness.DistributedLockComponentName;
40+
Assert.NotNull(componentName);
41+
42+
using var scope = testApp.CreateScope();
43+
var client = scope.ServiceProvider.GetRequiredService<DaprDistributedLockClient>();
44+
45+
var acquired = await client.TryLockAsync(componentName, resourceId, owner, expiryInSeconds: 10);
46+
Assert.NotNull(acquired);
47+
48+
var unlock = await client.TryUnlockAsync(componentName, resourceId, owner);
49+
Assert.Equal(LockStatus.Success, unlock.Status);
50+
}
51+
52+
[Fact]
53+
public async Task ShouldEnforceExclusivityAndReturnExpectedUnlockStatuses()
54+
{
55+
var options = new DaprRuntimeOptions();
56+
var componentsDir = TestDirectoryManager.CreateTestDirectory("distributedlock-components");
57+
var resourceId = $"resource-{Guid.NewGuid():N}";
58+
var owner1 = $"owner-{Guid.NewGuid():N}";
59+
var owner2 = $"owner-{Guid.NewGuid():N}";
60+
61+
await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync();
62+
await environment.StartAsync();
63+
64+
var harness = new DaprHarnessBuilder(options, environment).BuildDistributedLock(componentsDir);
65+
await using var testApp = await DaprHarnessBuilder.ForHarness(harness)
66+
.ConfigureServices(builder =>
67+
{
68+
builder.Services.AddDaprDistributedLock((sp, clientBuilder) =>
69+
{
70+
var config = sp.GetRequiredService<IConfiguration>();
71+
var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"];
72+
if (!string.IsNullOrEmpty(grpcEndpoint))
73+
clientBuilder.UseGrpcEndpoint(grpcEndpoint);
74+
});
75+
})
76+
.BuildAndStartAsync();
77+
78+
const string componentName = DistributedLockHarness.DistributedLockComponentName;
79+
Assert.NotNull(componentName);
80+
81+
using var scope = testApp.CreateScope();
82+
var client = scope.ServiceProvider.GetRequiredService<DaprDistributedLockClient>();
83+
84+
var lock1 = await client.TryLockAsync(componentName, resourceId, owner1, expiryInSeconds: 20);
85+
Assert.NotNull(lock1);
86+
87+
// While owner1 holds the lock, owner2 should not be able to acquire it.
88+
var lock2 = await client.TryLockAsync(componentName, resourceId, owner2, expiryInSeconds: 20);
89+
Assert.Null(lock2);
90+
91+
// Wrong owner tries to unlock -> should indicate ownership mismatch.
92+
var wrongUnlock = await client.TryUnlockAsync(componentName, resourceId, owner2);
93+
Assert.Equal(LockStatus.LockBelongsToOthers, wrongUnlock.Status);
94+
95+
// Correct owner unlocks -> success.
96+
var correctUnlock = await client.TryUnlockAsync(componentName, resourceId, owner1);
97+
Assert.Equal(LockStatus.Success, correctUnlock.Status);
98+
99+
// Unlocking again after release -> lock does not exist.
100+
var secondUnlock = await client.TryUnlockAsync(componentName, resourceId, owner1);
101+
Assert.Equal(LockStatus.LockDoesNotExist, secondUnlock.Status);
102+
}
103+
104+
[Fact]
105+
public async Task ShouldAllowAcquireAfterExpiry()
106+
{
107+
var options = new DaprRuntimeOptions();
108+
var componentsDir = TestDirectoryManager.CreateTestDirectory("distributedlock-components");
109+
var resourceId = $"resource-{Guid.NewGuid():N}";
110+
var owner1 = $"owner-{Guid.NewGuid():N}";
111+
var owner2 = $"owner-{Guid.NewGuid():N}";
112+
113+
await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync();
114+
await environment.StartAsync();
115+
116+
var harness = new DaprHarnessBuilder(options, environment).BuildDistributedLock(componentsDir);
117+
await using var testApp = await DaprHarnessBuilder.ForHarness(harness)
118+
.ConfigureServices(builder =>
119+
{
120+
builder.Services.AddDaprDistributedLock((sp, clientBuilder) =>
121+
{
122+
var config = sp.GetRequiredService<IConfiguration>();
123+
var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"];
124+
if (!string.IsNullOrEmpty(grpcEndpoint))
125+
clientBuilder.UseGrpcEndpoint(grpcEndpoint);
126+
});
127+
})
128+
.BuildAndStartAsync();
129+
130+
const string componentName = DistributedLockHarness.DistributedLockComponentName;
131+
Assert.NotNull(componentName);
132+
133+
using var scope = testApp.CreateScope();
134+
var client = scope.ServiceProvider.GetRequiredService<DaprDistributedLockClient>();
135+
136+
// Acquire a short-lived lock and *do not* unlock it.
137+
var first = await client.TryLockAsync(componentName, resourceId, owner1, expiryInSeconds: 2);
138+
Assert.NotNull(first);
139+
140+
// Poll until the lock becomes available and owner2 can acquire it.
141+
var acquiredByOwner2 = await WaitUntilAsync(
142+
async () => await client.TryLockAsync(componentName, resourceId, owner2, expiryInSeconds: 10),
143+
isSuccess: lr => lr is not null,
144+
timeout: TimeSpan.FromSeconds(30),
145+
pollInterval: TimeSpan.FromMilliseconds(250));
146+
147+
Assert.NotNull(acquiredByOwner2);
148+
149+
var unlock2 = await client.TryUnlockAsync(componentName, resourceId, owner2);
150+
Assert.Equal(LockStatus.Success, unlock2.Status);
151+
}
152+
153+
private static async Task<T?> WaitUntilAsync<T>(
154+
Func<Task<T?>> action,
155+
Func<T?, bool> isSuccess,
156+
TimeSpan timeout,
157+
TimeSpan pollInterval)
158+
{
159+
using var cts = new CancellationTokenSource(timeout);
160+
while (!cts.IsCancellationRequested)
161+
{
162+
var result = await action();
163+
if (isSuccess(result))
164+
return result;
165+
166+
await Task.Delay(pollInterval, cts.Token);
167+
}
168+
169+
throw new TimeoutException($"Condition was not met within {timeout}.");
170+
}
171+
}

test/Dapr.IntegrationTest.Jobs/Dapr.IntegrationTest.Jobs.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<ImplicitUsings>enable</ImplicitUsings>
55
<Nullable>enable</Nullable>
66
<IsPackable>false</IsPackable>
7-
<RootNamespace>Dapr.E2E.Test.Jobs</RootNamespace>
87
</PropertyGroup>
98

109
<ItemGroup>

test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
using Microsoft.Extensions.DependencyInjection;
2323
using Microsoft.Extensions.Logging;
2424

25-
namespace Dapr.E2E.Test.Jobs;
25+
namespace Dapr.IntegrationTest.Jobs;
2626

2727
public sealed class JobFailurePolicyTests
2828
{

0 commit comments

Comments
 (0)