w4c-workflows-api/w4c-workflows-api.Tests/SyncGateTests.cs

56 lines
2 KiB
C#
Raw Permalink Normal View History

2026-09-13 16:28:47 +00:00
using w4c_workflows.Services;
using Xunit;
namespace w4c_workflows.Tests;
/// <summary>
/// P1-9: two overlapping syncs for the same <c>(tenant, repo)</c> must not run
/// concurrently, or they insert the same deterministic workflow/task ids and
/// collide on the primary key. The gate serializes that pair only — syncs for a
/// different repo/tenant stay parallel.
/// </summary>
public class SyncGateTests
{
[Fact]
public async Task Same_tenant_and_repo_waits_for_the_holder()
{
var gate = new SyncGate();
var held = await gate.AcquireAsync("tenant-a", "repo", default);
var waiter = gate.AcquireAsync("tenant-a", "repo", default);
Assert.False(waiter.IsCompleted);
held.Dispose();
using var acquired = await waiter.WaitAsync(TimeSpan.FromSeconds(2));
Assert.True(waiter.IsCompletedSuccessfully);
}
[Fact]
public async Task Different_keys_do_not_block_each_other()
{
var gate = new SyncGate();
using var first = await gate.AcquireAsync("tenant-a", "repo", default);
// Another repo for the same tenant (and another tenant for the same repo)
// must not wait behind the held gate.
using var otherRepo = await gate.AcquireAsync("tenant-a", "other-repo", default).WaitAsync(TimeSpan.FromSeconds(2));
using var otherTenant = await gate.AcquireAsync("tenant-b", "repo", default).WaitAsync(TimeSpan.FromSeconds(2));
Assert.NotNull(otherRepo);
Assert.NotNull(otherTenant);
}
[Fact]
public async Task Dispose_is_idempotent_and_releases_once()
{
var gate = new SyncGate();
var held = await gate.AcquireAsync("tenant-a", "repo", default);
held.Dispose();
held.Dispose();
// A second acquisition after the (double) release must still succeed.
using var next = await gate.AcquireAsync("tenant-a", "repo", default).WaitAsync(TimeSpan.FromSeconds(2));
Assert.NotNull(next);
}
}