Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4db41e7
Add Dapr.IntegrationTest.Actors project and fix ActorHarness
Copilot Apr 12, 2026
08fddef
Fix ActorHarness Redis disposal and TimerActor timer name tracking
Copilot Apr 12, 2026
39f1e5c
Address review feedback: 2026 copyright, primary constructors, switch…
Copilot Apr 13, 2026
bfcd903
test: add comprehensive ActorStateManager, DaprStateProvider, ActorRu…
Copilot Apr 13, 2026
458f903
test: remove unused System.Net using from ActivationTests.cs
Copilot Apr 13, 2026
80f9701
fix: pass IConfiguration to GetDefaultHttpEndpoint in ActorsServiceCo…
Copilot Apr 13, 2026
3ad5781
style: fix spelling normalises -> normalizes in test comment
Copilot Apr 13, 2026
79b4c15
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 14, 2026
7e36f56
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 14, 2026
dd0d3aa
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 14, 2026
209a127
Added missing dispose statement
WhitWaldo Apr 15, 2026
9ef7a54
Simplified actor harness setup
WhitWaldo Apr 15, 2026
375dfeb
Removed extraneous arguments from harness builder
WhitWaldo Apr 15, 2026
e9fea7f
Fixing integration test approach in light of removed unused argument
WhitWaldo Apr 15, 2026
410434f
fix: bound WaitForActorRuntimeAsync per-attempt to 5s to avoid 100s H…
Copilot Apr 15, 2026
3c48728
style: restore 250ms retry delay in WaitForActorRuntimeAsync (reverts…
Copilot Apr 15, 2026
036a216
fix: environment lifecycle bug in CreateTestAppAsync — placement/sche…
Copilot Apr 15, 2026
fa92455
style: US English spelling in ActorTestContext XML doc (Initializes)
Copilot Apr 15, 2026
6b882d6
fix: actor integration test failures — TTL feature flag, serializatio…
Copilot Apr 15, 2026
1e4e1b6
style: add System.IO using to ActorHarness, narrow DaprApiException c…
Copilot Apr 15, 2026
8c7a6f3
Merge branch 'master' into copilot/add-dapr-integrationtest-actors
WhitWaldo Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Messag
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Cryptography", "test\Dapr.IntegrationTest.Cryptography\Dapr.IntegrationTest.Cryptography.csproj", "{7B14879F-156B-417E-ACA3-0B5A69CC2F39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Actors", "test\Dapr.IntegrationTest.Actors\Dapr.IntegrationTest.Actors.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -669,6 +671,10 @@ Global
{7B14879F-156B-417E-ACA3-0B5A69CC2F39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B14879F-156B-417E-ACA3-0B5A69CC2F39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B14879F-156B-417E-ACA3-0B5A69CC2F39}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -791,6 +797,7 @@ Global
{97CAEE0B-4020-4A86-97DA-9900FDF4DFC6} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{01A20A89-53A1-4D5B-B563-89E157718474} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{7B14879F-156B-417E-ACA3-0B5A69CC2F39} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,6 @@ private static void ConfigureActorOptions(IServiceProvider serviceProvider, Acto
: DaprDefaults.GetDefaultDaprApiToken(configuration);
options.HttpEndpoint = !string.IsNullOrWhiteSpace(options.HttpEndpoint)
? options.HttpEndpoint
: DaprDefaults.GetDefaultHttpEndpoint();
: DaprDefaults.GetDefaultHttpEndpoint(configuration);
}
}
18 changes: 5 additions & 13 deletions src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,12 @@ public CryptographyHarness BuildCryptography(string keysDir) =>
/// <summary>
/// Builds a PubSub harness.
/// </summary>
/// <param name="componentsDir">The path to the Dapr resources.</param>
public PubSubHarness BuildPubSub(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
public PubSubHarness BuildPubSub() => new(_componentsDirectory, _startApp, _options, _environment);

// /// <summary>
// /// Builds a state management harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public StateManagementHarness BuildStateManagement(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
//
// /// <summary>
// /// Builds an actor harness.
// /// </summary>
// /// <param name="componentsDir">The path to the Dapr resources.</param>
// public ActorHarness BuildActors(string componentsDir) => new(_componentsDirectory, _startApp, _options, _environment);
/// <summary>
/// Builds an actor harness.
/// </summary>
public ActorHarness BuildActors() => new(_componentsDirectory, _startApp, _options, _environment);

/// <summary>
/// Creates a test application builder for the specified harness.
Expand Down
10 changes: 9 additions & 1 deletion src/Dapr.Testcontainers/Containers/Dapr/DaprdContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public sealed class DaprdContainer : IAsyncStartable
/// <param name="daprHttpPort">The host HTTP port to bind to.</param>
/// <param name="daprGrpcPort">The host gRPC port to bind to.</param>
/// <param name="logDirectory">The directory to write container logs to.</param>
/// <param name="configFilePath">The path inside the container of an optional Dapr configuration YAML file.</param>
public DaprdContainer(
string appId,
string componentsHostFolder,
Expand All @@ -80,7 +81,8 @@ public DaprdContainer(
HostPortPair? schedulerHostAndPort = null,
int? daprHttpPort = null,
int? daprGrpcPort = null,
string? logDirectory = null
string? logDirectory = null,
string? configFilePath = null
)
{
_requestedHttpPort = daprHttpPort;
Expand All @@ -101,6 +103,12 @@ public DaprdContainer(
"-resources-path", componentsPath
};

if (configFilePath is not null)
{
cmd.Add("-config");
cmd.Add(configFilePath);
}

if (placementHostAndPort is not null)
{
cmd.Add("-placement-host-address");
Expand Down
87 changes: 58 additions & 29 deletions src/Dapr.Testcontainers/Harnesses/ActorHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
// ------------------------------------------------------------------------

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Testcontainers.Common.Options;
using Dapr.Testcontainers.Containers;
using Dapr.Testcontainers.Containers.Dapr;

namespace Dapr.Testcontainers.Harnesses;

Expand All @@ -26,45 +26,74 @@ namespace Dapr.Testcontainers.Harnesses;
public sealed class ActorHarness : BaseHarness
{
private readonly RedisContainer _redis;
private readonly DaprPlacementContainer _placement;
private readonly DaprSchedulerContainer _schedueler;
private readonly string componentsDir;
private readonly bool _isSelfHostedRedis;

/// <summary>
/// Provides an implementation harness for Dapr's actor building block.
/// </summary>
/// <param name="componentsDir">The directory to Dapr components.</param>
/// <param name="startApp">The test app to validate in the harness.</param>
/// <param name="options">The dapr runtime options.</param>
/// <param name="environment">The isolated environment instance.</param>
public ActorHarness(string componentsDir, Func<int, Task>? startApp, DaprRuntimeOptions options, DaprTestEnvironment? environment = null) : base(componentsDir, startApp, options, environment)
/// <param name="options">The Dapr runtime options.</param>
/// <param name="environment">
/// An optional shared <see cref="DaprTestEnvironment"/>. When provided the harness reuses
/// its Redis, Placement, and Scheduler services instead of starting its own.
/// </param>
public ActorHarness(string componentsDir, Func<int, Task>? startApp, DaprRuntimeOptions options, DaprTestEnvironment? environment = null)
: base(componentsDir, startApp, options, environment)
{
this.componentsDir = componentsDir;
_placement = new DaprPlacementContainer(options, Network, ContainerLogsDirectory);
_schedueler = new DaprSchedulerContainer(options, Network, ContainerLogsDirectory);
_redis = new RedisContainer(Network, ContainerLogsDirectory);
_redis = environment?.RedisContainer ?? new RedisContainer(Network, ContainerLogsDirectory);
_isSelfHostedRedis = environment?.RedisContainer is null;
}

/// <inheritdoc />
protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
{
// Start infrastructure
await _redis.StartAsync(cancellationToken);
await _placement.StartAsync(cancellationToken);
await _schedueler.StartAsync(cancellationToken);

// Emit component YAMLs pointing to Redis
RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");
protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
{
// Only start Redis if it is not provided by a shared environment.
if (_isSelfHostedRedis)
{
await _redis.StartAsync(cancellationToken);
}

// Write the state-store component YAML that points to the Redis instance.
RedisContainer.Yaml.WriteStateStoreYamlToFolder(
ComponentsDirectory,
redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");

// Write a Dapr configuration file that enables the ActorStateTTL feature.
WriteActorConfigYaml(ComponentsDirectory);
DaprConfigFilePath = "/components/actor-config.yaml";

DaprPlacementExternalPort = _placement.ExternalPort;
DaprSchedulerExternalPort = _schedueler.ExternalPort;
// Forward placement and scheduler coordinates from the environment.
DaprPlacementExternalPort = Environment.PlacementExternalPort;
DaprPlacementAlias = Environment.PlacementAlias;
DaprSchedulerExternalPort = Environment.SchedulerExternalPort;
DaprSchedulerAlias = Environment.SchedulerAlias;
}


private static void WriteActorConfigYaml(string componentsDirectory)
{
const string yaml = """
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: actorConfig
spec:
features:
- name: "ActorStateTTL"
enabled: true
""";
Directory.CreateDirectory(componentsDirectory);
File.WriteAllText(
Path.Combine(componentsDirectory, "actor-config.yaml"),
yaml);
}

/// <inheritdoc />
protected override async ValueTask OnDisposeAsync()
{
await _redis.DisposeAsync();
await _placement.DisposeAsync();
await _schedueler.DisposeAsync();
}
protected override async ValueTask OnDisposeAsync()
{
if (_isSelfHostedRedis)
{
await _redis.DisposeAsync();
}
}
}
10 changes: 9 additions & 1 deletion src/Dapr.Testcontainers/Harnesses/BaseHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ public void SetAppPort(int appPort)
/// </summary>
protected string? DaprSchedulerAlias { get; set; }

/// <summary>
/// The path inside the Dapr sidecar container of an optional Dapr configuration YAML file.
/// Set this in <see cref="OnInitializeAsync"/> before the base class creates the sidecar.
/// When set, daprd is started with <c>-config &lt;path&gt;</c>.
/// </summary>
protected string? DaprConfigFilePath { get; set; }

/// <summary>
/// Pre-assigns the Dapr ports to use. This is useful when the app starts before the Dapr container.
/// </summary>
Expand Down Expand Up @@ -222,7 +229,8 @@ DaprSchedulerExternalPort is null || DaprSchedulerAlias is null
? null : new HostPortPair(DaprSchedulerAlias, DaprSchedulerContainer.InternalPort),
_daprHttpPortOverride,
_daprGrpcPortOverride,
_logDirectory);
_logDirectory,
DaprConfigFilePath);

var daprdTask = Task.Run(async () =>
{
Expand Down
8 changes: 7 additions & 1 deletion src/Dapr.Testcontainers/Harnesses/WorkflowHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,11 @@ protected override async Task OnInitializeAsync(CancellationToken cancellationTo
}

/// <inheritdoc />
protected override ValueTask OnDisposeAsync() => ValueTask.CompletedTask;
protected override async ValueTask OnDisposeAsync()
{
if (_isSelfHostedRedis)
{
await _redis.DisposeAsync();
}
}
}
39 changes: 39 additions & 0 deletions test/Dapr.Actors.AspNetCore.IntegrationTest/ActivationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// ------------------------------------------------------------------------

using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
Expand Down Expand Up @@ -45,6 +46,44 @@ public async Task CanActivateActorWithDependencyInjection()
await DeactivateActor(httpClient, "A");
}

[Fact]
public async Task InvokeMethod_UnregisteredActorType_ReturnsNonSuccessResponse()
{
await using var factory = new AppWebApplicationFactory();
var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false });

var request = new HttpRequestMessage(HttpMethod.Put,
"http://localhost/actors/NoSuchActorType/someId/method/SomeMethod");
var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken);

Assert.False(response.IsSuccessStatusCode,
$"Expected a non-success status code for an unregistered actor type, but got {response.StatusCode}");
}

[Fact]
public async Task ConcurrentIncrement_SameActorId_RetainsConsistentState()
{
await using var factory = new AppWebApplicationFactory();
var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false });

const string actorId = "concurrent-actor";
const int concurrency = 5;

// Fire `concurrency` increments at the same actor ID in parallel.
var tasks = Enumerable.Range(0, concurrency)
.Select(_ => IncrementCounterAsync(httpClient, actorId))
.ToArray();

var results = await Task.WhenAll(tasks);

// Each call should have received a distinct integer from 0 to concurrency-1, proving
// that the actor serialised the concurrent requests and no two calls returned the same value.
var distinct = results.Select(int.Parse).OrderBy(x => x).ToArray();
Assert.Equal(Enumerable.Range(0, concurrency).ToArray(), distinct);

await DeactivateActor(httpClient, actorId);
}

private async Task<string> IncrementCounterAsync(HttpClient httpClient, string actorId)
{
const string actorTypeName = nameof(DependencyInjectionActor);
Expand Down
74 changes: 74 additions & 0 deletions test/Dapr.Actors.AspNetCore.Test/ActorHostingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
// limitations under the License.
// ------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Dapr.Actors.Client;
using Dapr.Actors.Runtime;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

Expand Down Expand Up @@ -107,4 +110,75 @@ public TestActor2(ActorHost host)
{
}
}
}

/// <summary>
/// Tests for AddActors — verifying HttpEndpoint and DaprApiToken options are propagated to the
/// resolved <see cref="IActorProxyFactory"/>.
/// </summary>
public class ActorServiceCollectionExtensionsOptionsTests
{
[Fact]
public void AddActors_HttpEndpoint_IsReflectedInProxyFactory()
{
const string endpoint = "http://my-custom-dapr:3501";

var services = new ServiceCollection();
services.AddLogging();
services.AddOptions();
services.AddActors(options =>
{
options.HttpEndpoint = endpoint;
});

var factory = (ActorProxyFactory)services.BuildServiceProvider().GetRequiredService<IActorProxyFactory>();
Assert.Equal(endpoint, factory.DefaultOptions.HttpEndpoint);
}

[Fact]
public void AddActors_DaprApiToken_IsReflectedInProxyFactory()
{
const string token = "super-secret-token";

var services = new ServiceCollection();
services.AddLogging();
services.AddOptions();
services.AddActors(options =>
{
options.DaprApiToken = token;
});

var factory = (ActorProxyFactory)services.BuildServiceProvider().GetRequiredService<IActorProxyFactory>();
Assert.Equal(token, factory.DefaultOptions.DaprApiToken);
}

[Fact]
public void AddActors_HttpEndpointFromConfiguration_IsReflectedInProxyFactory()
{
// Verify that when no explicit HttpEndpoint is set, AddActors falls back to
// the DAPR_HTTP_ENDPOINT key in IConfiguration (e.g. in-memory config, not env var).
const string endpoint = "http://dapr-from-config:3502";

var services = new ServiceCollection();
services.AddLogging();
services.AddOptions();

// Populate IConfiguration with the endpoint — simulates what DaprTestApplicationBuilder does.
services.AddSingleton<IConfiguration>(
new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "DAPR_HTTP_ENDPOINT", endpoint }
})
.Build());

services.AddActors(options =>
{
// HttpEndpoint is intentionally NOT set; the fallback should read from IConfiguration.
});

var factory = (ActorProxyFactory)services.BuildServiceProvider().GetRequiredService<IActorProxyFactory>();
// GetDefaultHttpEndpoint normalizes the URL and may append a trailing slash.
Assert.StartsWith(endpoint, factory.DefaultOptions.HttpEndpoint, StringComparison.Ordinal);
}
}
Loading
Loading