up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.TaskRunner.Core.Tenancy;
|
||||
using StellaOps.TaskRunner.Infrastructure.Tenancy;
|
||||
|
||||
namespace StellaOps.TaskRunner.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for tenant enforcement per TASKRUN-TEN-48-001.
|
||||
/// </summary>
|
||||
public sealed class TenantEnforcementTests
|
||||
{
|
||||
#region TenantContext Tests
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_RequiresTenantId()
|
||||
{
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
new TenantContext(null!, "project-1"));
|
||||
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
new TenantContext("", "project-1"));
|
||||
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
new TenantContext(" ", "project-1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_RequiresProjectId()
|
||||
{
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
new TenantContext("tenant-1", null!));
|
||||
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
new TenantContext("tenant-1", ""));
|
||||
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
new TenantContext("tenant-1", " "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_TrimsIds()
|
||||
{
|
||||
var context = new TenantContext(" tenant-1 ", " project-1 ");
|
||||
|
||||
Assert.Equal("tenant-1", context.TenantId);
|
||||
Assert.Equal("project-1", context.ProjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_GeneratesStoragePrefix()
|
||||
{
|
||||
var context = new TenantContext("Tenant-1", "Project-1");
|
||||
|
||||
Assert.Equal("tenant-1/project-1", context.StoragePrefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_GeneratesFlatPrefix()
|
||||
{
|
||||
var context = new TenantContext("Tenant-1", "Project-1");
|
||||
|
||||
Assert.Equal("tenant-1_project-1", context.FlatPrefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_GeneratesLoggingScope()
|
||||
{
|
||||
var context = new TenantContext("tenant-1", "project-1");
|
||||
var scope = context.ToLoggingScope();
|
||||
|
||||
Assert.Equal("tenant-1", scope["TenantId"]);
|
||||
Assert.Equal("project-1", scope["ProjectId"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenantContext_DefaultRestrictionsAreNone()
|
||||
{
|
||||
var context = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
Assert.False(context.Restrictions.EgressBlocked);
|
||||
Assert.False(context.Restrictions.ReadOnly);
|
||||
Assert.False(context.Restrictions.Suspended);
|
||||
Assert.Null(context.Restrictions.MaxConcurrentRuns);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region StoragePathResolver Tests
|
||||
|
||||
[Fact]
|
||||
public void StoragePathResolver_HierarchicalPaths()
|
||||
{
|
||||
var options = new TenantStoragePathOptions
|
||||
{
|
||||
PathStrategy = TenantPathStrategy.Hierarchical,
|
||||
StateBasePath = "state",
|
||||
LogsBasePath = "logs"
|
||||
};
|
||||
|
||||
var resolver = new TenantScopedStoragePathResolver(options, "/data");
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
var statePath = resolver.GetStatePath(tenant, "run-123");
|
||||
var logsPath = resolver.GetLogsPath(tenant, "run-123");
|
||||
|
||||
Assert.Contains("state", statePath);
|
||||
Assert.Contains("tenant-1", statePath);
|
||||
Assert.Contains("project-1", statePath);
|
||||
Assert.Contains("run-123", statePath);
|
||||
|
||||
Assert.Contains("logs", logsPath);
|
||||
Assert.Contains("tenant-1", logsPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoragePathResolver_FlatPaths()
|
||||
{
|
||||
var options = new TenantStoragePathOptions
|
||||
{
|
||||
PathStrategy = TenantPathStrategy.Flat,
|
||||
StateBasePath = "state"
|
||||
};
|
||||
|
||||
var resolver = new TenantScopedStoragePathResolver(options, "/data");
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
var statePath = resolver.GetStatePath(tenant, "run-123");
|
||||
|
||||
Assert.Contains("tenant-1_project-1_run-123", statePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoragePathResolver_HashedPaths()
|
||||
{
|
||||
var options = new TenantStoragePathOptions
|
||||
{
|
||||
PathStrategy = TenantPathStrategy.Hashed
|
||||
};
|
||||
|
||||
var resolver = new TenantScopedStoragePathResolver(options, "/data");
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
var basePath = resolver.GetTenantBasePath(tenant);
|
||||
|
||||
// Should contain a hash (hex characters)
|
||||
Assert.DoesNotContain("tenant-1", basePath);
|
||||
Assert.Contains("project-1", basePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoragePathResolver_ValidatesPathOwnership()
|
||||
{
|
||||
var options = new TenantStoragePathOptions
|
||||
{
|
||||
PathStrategy = TenantPathStrategy.Hierarchical
|
||||
};
|
||||
|
||||
// Use temp path for cross-platform compatibility
|
||||
var basePath = Path.Combine(Path.GetTempPath(), "tenant-test-" + Guid.NewGuid().ToString("N")[..8]);
|
||||
var resolver = new TenantScopedStoragePathResolver(options, basePath);
|
||||
var tenant1 = new TenantContext("tenant-1", "project-1");
|
||||
var tenant2 = new TenantContext("tenant-2", "project-1");
|
||||
|
||||
var tenant1Path = resolver.GetStatePath(tenant1, "run-123");
|
||||
var tenant2Path = resolver.GetStatePath(tenant2, "run-123");
|
||||
|
||||
Assert.True(resolver.ValidatePathBelongsToTenant(tenant1, tenant1Path));
|
||||
Assert.False(resolver.ValidatePathBelongsToTenant(tenant1, tenant2Path));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EgressPolicy Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_AllowsByDefault()
|
||||
{
|
||||
var options = new TenantEgressPolicyOptions { AllowByDefault = true };
|
||||
var policy = CreateEgressPolicy(options);
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
var result = await policy.CheckEgressAsync(tenant, "example.com", 443);
|
||||
|
||||
Assert.True(result.IsAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_BlocksGlobalBlocklist()
|
||||
{
|
||||
var options = new TenantEgressPolicyOptions
|
||||
{
|
||||
AllowByDefault = true,
|
||||
GlobalBlocklist = ["blocked.com"]
|
||||
};
|
||||
var policy = CreateEgressPolicy(options);
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
var result = await policy.CheckEgressAsync(tenant, "blocked.com", 443);
|
||||
|
||||
Assert.False(result.IsAllowed);
|
||||
Assert.Equal(EgressBlockReason.GlobalPolicy, result.BlockReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_BlocksSuspendedTenants()
|
||||
{
|
||||
var options = new TenantEgressPolicyOptions { AllowByDefault = true };
|
||||
var policy = CreateEgressPolicy(options);
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions { Suspended = true });
|
||||
|
||||
var result = await policy.CheckEgressAsync(tenant, "example.com", 443);
|
||||
|
||||
Assert.False(result.IsAllowed);
|
||||
Assert.Equal(EgressBlockReason.TenantSuspended, result.BlockReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_BlocksRestrictedTenants()
|
||||
{
|
||||
var options = new TenantEgressPolicyOptions { AllowByDefault = true };
|
||||
var policy = CreateEgressPolicy(options);
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions { EgressBlocked = true });
|
||||
|
||||
var result = await policy.CheckEgressAsync(tenant, "example.com", 443);
|
||||
|
||||
Assert.False(result.IsAllowed);
|
||||
Assert.Equal(EgressBlockReason.TenantRestriction, result.BlockReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_AllowsRestrictedTenantAllowlist()
|
||||
{
|
||||
var options = new TenantEgressPolicyOptions { AllowByDefault = true };
|
||||
var policy = CreateEgressPolicy(options);
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions
|
||||
{
|
||||
EgressBlocked = true,
|
||||
AllowedEgressDomains = ["allowed.com"]
|
||||
});
|
||||
|
||||
var allowedResult = await policy.CheckEgressAsync(tenant, "allowed.com", 443);
|
||||
var blockedResult = await policy.CheckEgressAsync(tenant, "other.com", 443);
|
||||
|
||||
Assert.True(allowedResult.IsAllowed);
|
||||
Assert.False(blockedResult.IsAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_SupportsWildcardDomains()
|
||||
{
|
||||
var options = new TenantEgressPolicyOptions
|
||||
{
|
||||
AllowByDefault = true,
|
||||
GlobalBlocklist = ["*.blocked.com"]
|
||||
};
|
||||
var policy = CreateEgressPolicy(options);
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
var result = await policy.CheckEgressAsync(tenant, "sub.blocked.com", 443);
|
||||
|
||||
Assert.False(result.IsAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EgressPolicy_RecordsAttempts()
|
||||
{
|
||||
var auditLog = new InMemoryEgressAuditLog();
|
||||
var options = new TenantEgressPolicyOptions
|
||||
{
|
||||
AllowByDefault = true,
|
||||
LogBlockedAttempts = true
|
||||
};
|
||||
var policy = CreateEgressPolicy(options, auditLog);
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
var uri = new Uri("https://example.com/api");
|
||||
|
||||
var result = await policy.CheckEgressAsync(tenant, uri);
|
||||
await policy.RecordEgressAttemptAsync(tenant, "run-123", uri, result);
|
||||
|
||||
var records = auditLog.GetAllRecords();
|
||||
Assert.Single(records);
|
||||
Assert.Equal("tenant-1", records[0].TenantId);
|
||||
Assert.Equal("run-123", records[0].RunId);
|
||||
Assert.True(records[0].WasAllowed);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TenantEnforcer Tests
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_RequiresTenantId()
|
||||
{
|
||||
var enforcer = CreateTenantEnforcer();
|
||||
var request = new PackRunTenantRequest("", "project-1");
|
||||
|
||||
var result = await enforcer.ValidateRequestAsync(request);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TenantEnforcementFailureKind.MissingTenantId, result.FailureKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_RequiresProjectId()
|
||||
{
|
||||
var options = new TenancyEnforcementOptions { RequireProjectId = true };
|
||||
var enforcer = CreateTenantEnforcer(options);
|
||||
var request = new PackRunTenantRequest("tenant-1", "");
|
||||
|
||||
var result = await enforcer.ValidateRequestAsync(request);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TenantEnforcementFailureKind.MissingProjectId, result.FailureKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_BlocksSuspendedTenants()
|
||||
{
|
||||
var tenantProvider = new InMemoryTenantContextProvider();
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions { Suspended = true });
|
||||
tenantProvider.Register(tenant);
|
||||
|
||||
var options = new TenancyEnforcementOptions { BlockSuspendedTenants = true };
|
||||
var enforcer = CreateTenantEnforcer(options, tenantProvider);
|
||||
var request = new PackRunTenantRequest("tenant-1", "project-1");
|
||||
|
||||
var result = await enforcer.ValidateRequestAsync(request);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TenantEnforcementFailureKind.TenantSuspended, result.FailureKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_BlocksReadOnlyTenants()
|
||||
{
|
||||
var tenantProvider = new InMemoryTenantContextProvider();
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions { ReadOnly = true });
|
||||
tenantProvider.Register(tenant);
|
||||
|
||||
var enforcer = CreateTenantEnforcer(tenantProvider: tenantProvider);
|
||||
var request = new PackRunTenantRequest("tenant-1", "project-1");
|
||||
|
||||
var result = await enforcer.ValidateRequestAsync(request);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TenantEnforcementFailureKind.TenantReadOnly, result.FailureKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_EnforcesConcurrentRunLimit()
|
||||
{
|
||||
var tenantProvider = new InMemoryTenantContextProvider();
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions { MaxConcurrentRuns = 2 });
|
||||
tenantProvider.Register(tenant);
|
||||
|
||||
var runTracker = new InMemoryConcurrentRunTracker();
|
||||
await runTracker.IncrementAsync("tenant-1", "run-1");
|
||||
await runTracker.IncrementAsync("tenant-1", "run-2");
|
||||
|
||||
var enforcer = CreateTenantEnforcer(tenantProvider: tenantProvider, runTracker: runTracker);
|
||||
var request = new PackRunTenantRequest("tenant-1", "project-1");
|
||||
|
||||
var result = await enforcer.ValidateRequestAsync(request);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(TenantEnforcementFailureKind.MaxConcurrentRunsReached, result.FailureKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_AllowsWithinConcurrentLimit()
|
||||
{
|
||||
var tenantProvider = new InMemoryTenantContextProvider();
|
||||
var tenant = new TenantContext(
|
||||
"tenant-1",
|
||||
"project-1",
|
||||
restrictions: new TenantRestrictions { MaxConcurrentRuns = 5 });
|
||||
tenantProvider.Register(tenant);
|
||||
|
||||
var runTracker = new InMemoryConcurrentRunTracker();
|
||||
await runTracker.IncrementAsync("tenant-1", "run-1");
|
||||
|
||||
var enforcer = CreateTenantEnforcer(tenantProvider: tenantProvider, runTracker: runTracker);
|
||||
var request = new PackRunTenantRequest("tenant-1", "project-1");
|
||||
|
||||
var result = await enforcer.ValidateRequestAsync(request);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.NotNull(result.Tenant);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_TracksRunStartCompletion()
|
||||
{
|
||||
var runTracker = new InMemoryConcurrentRunTracker();
|
||||
var enforcer = CreateTenantEnforcer(runTracker: runTracker);
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
|
||||
await enforcer.RecordRunStartAsync(tenant, "run-1");
|
||||
Assert.Equal(1, await enforcer.GetConcurrentRunCountAsync(tenant));
|
||||
|
||||
await enforcer.RecordRunStartAsync(tenant, "run-2");
|
||||
Assert.Equal(2, await enforcer.GetConcurrentRunCountAsync(tenant));
|
||||
|
||||
await enforcer.RecordRunCompletionAsync(tenant, "run-1");
|
||||
Assert.Equal(1, await enforcer.GetConcurrentRunCountAsync(tenant));
|
||||
|
||||
await enforcer.RecordRunCompletionAsync(tenant, "run-2");
|
||||
Assert.Equal(0, await enforcer.GetConcurrentRunCountAsync(tenant));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_CreatesExecutionContext()
|
||||
{
|
||||
var tenantProvider = new InMemoryTenantContextProvider();
|
||||
var tenant = new TenantContext("tenant-1", "project-1");
|
||||
tenantProvider.Register(tenant);
|
||||
|
||||
var enforcer = CreateTenantEnforcer(tenantProvider: tenantProvider);
|
||||
var request = new PackRunTenantRequest("tenant-1", "project-1");
|
||||
|
||||
var context = await enforcer.CreateExecutionContextAsync(request, "run-123");
|
||||
|
||||
Assert.NotNull(context);
|
||||
Assert.Equal("tenant-1", context.Tenant.TenantId);
|
||||
Assert.Equal("project-1", context.Tenant.ProjectId);
|
||||
Assert.NotNull(context.StoragePaths);
|
||||
Assert.Contains("tenant-1", context.LoggingScope["TenantId"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TenantEnforcer_ThrowsOnInvalidRequest()
|
||||
{
|
||||
var enforcer = CreateTenantEnforcer();
|
||||
var request = new PackRunTenantRequest("", "project-1");
|
||||
|
||||
await Assert.ThrowsAsync<TenantEnforcementException>(() =>
|
||||
enforcer.CreateExecutionContextAsync(request, "run-123").AsTask());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ConcurrentRunTracker Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentRunTracker_TracksMultipleTenants()
|
||||
{
|
||||
var tracker = new InMemoryConcurrentRunTracker();
|
||||
|
||||
await tracker.IncrementAsync("tenant-1", "run-1");
|
||||
await tracker.IncrementAsync("tenant-1", "run-2");
|
||||
await tracker.IncrementAsync("tenant-2", "run-3");
|
||||
|
||||
Assert.Equal(2, await tracker.GetCountAsync("tenant-1"));
|
||||
Assert.Equal(1, await tracker.GetCountAsync("tenant-2"));
|
||||
Assert.Equal(0, await tracker.GetCountAsync("tenant-3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentRunTracker_PreventsDoubleIncrement()
|
||||
{
|
||||
var tracker = new InMemoryConcurrentRunTracker();
|
||||
|
||||
await tracker.IncrementAsync("tenant-1", "run-1");
|
||||
await tracker.IncrementAsync("tenant-1", "run-1"); // Same run ID
|
||||
|
||||
Assert.Equal(1, await tracker.GetCountAsync("tenant-1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentRunTracker_HandlesNonExistentDecrement()
|
||||
{
|
||||
var tracker = new InMemoryConcurrentRunTracker();
|
||||
|
||||
// Should not throw
|
||||
await tracker.DecrementAsync("tenant-1", "non-existent");
|
||||
|
||||
Assert.Equal(0, await tracker.GetCountAsync("tenant-1"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static TenantEgressPolicy CreateEgressPolicy(
|
||||
TenantEgressPolicyOptions? options = null,
|
||||
IEgressAuditLog? auditLog = null)
|
||||
{
|
||||
return new TenantEgressPolicy(
|
||||
options ?? new TenantEgressPolicyOptions(),
|
||||
auditLog ?? NullEgressAuditLog.Instance,
|
||||
NullLogger<TenantEgressPolicy>.Instance);
|
||||
}
|
||||
|
||||
private static PackRunTenantEnforcer CreateTenantEnforcer(
|
||||
TenancyEnforcementOptions? options = null,
|
||||
ITenantContextProvider? tenantProvider = null,
|
||||
IConcurrentRunTracker? runTracker = null)
|
||||
{
|
||||
var storageOptions = new TenantStoragePathOptions();
|
||||
var pathResolver = new TenantScopedStoragePathResolver(storageOptions, Path.GetTempPath());
|
||||
|
||||
return new PackRunTenantEnforcer(
|
||||
tenantProvider ?? new InMemoryTenantContextProvider(),
|
||||
pathResolver,
|
||||
options ?? new TenancyEnforcementOptions { ValidateTenantExists = false },
|
||||
runTracker ?? new InMemoryConcurrentRunTracker(),
|
||||
NullLogger<PackRunTenantEnforcer>.Instance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user