using FluentAssertions; using StellaOps.Router.Gateway.RateLimit; using Xunit; namespace StellaOps.Router.Gateway.Tests; public sealed class LimitInheritanceResolverTests { [Fact] public void ResolveEnvironmentTarget_UsesEnvironmentDefaultsWhenNoMicroserviceOverride() { var config = new RateLimitConfig { ActivationThresholdPer5Min = 0, ForEnvironment = new EnvironmentLimitsConfig { ValkeyConnection = "localhost:6379", ValkeyBucket = "bucket", Rules = [new RateLimitRule { PerSeconds = 60, MaxRequests = 600 }], } }.Validate(); var resolver = new LimitInheritanceResolver(config); var target = resolver.ResolveEnvironmentTarget("scanner", "/api/scans"); target.Enabled.Should().BeTrue(); target.Kind.Should().Be(RateLimitTargetKind.EnvironmentDefault); target.RouteName.Should().BeNull(); target.TargetKey.Should().Be("scanner"); target.Rules.Should().ContainSingle(); target.Rules[0].PerSeconds.Should().Be(60); target.Rules[0].MaxRequests.Should().Be(600); } [Fact] public void ResolveEnvironmentTarget_UsesMicroserviceOverrideWhenPresent() { var config = new RateLimitConfig { ActivationThresholdPer5Min = 0, ForEnvironment = new EnvironmentLimitsConfig { ValkeyConnection = "localhost:6379", ValkeyBucket = "bucket", Rules = [new RateLimitRule { PerSeconds = 60, MaxRequests = 600 }], Microservices = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["scanner"] = new MicroserviceLimitsConfig { Rules = [new RateLimitRule { PerSeconds = 10, MaxRequests = 1 }], } } } }.Validate(); var resolver = new LimitInheritanceResolver(config); var target = resolver.ResolveEnvironmentTarget("scanner", "/api/scans"); target.Enabled.Should().BeTrue(); target.Kind.Should().Be(RateLimitTargetKind.Microservice); target.RouteName.Should().BeNull(); target.TargetKey.Should().Be("scanner"); target.Rules.Should().ContainSingle(); target.Rules[0].PerSeconds.Should().Be(10); target.Rules[0].MaxRequests.Should().Be(1); } [Fact] public void ResolveEnvironmentTarget_DisablesWhenNoRulesAtAnyLevel() { var config = new RateLimitConfig { ActivationThresholdPer5Min = 0, ForEnvironment = new EnvironmentLimitsConfig { ValkeyConnection = "localhost:6379", ValkeyBucket = "bucket", } }.Validate(); var resolver = new LimitInheritanceResolver(config); var target = resolver.ResolveEnvironmentTarget("scanner", "/api/scans"); target.Enabled.Should().BeFalse(); } [Fact] public void ResolveEnvironmentTarget_UsesRouteOverrideWhenPresent() { var config = new RateLimitConfig { ActivationThresholdPer5Min = 0, ForEnvironment = new EnvironmentLimitsConfig { ValkeyConnection = "localhost:6379", ValkeyBucket = "bucket", Rules = [new RateLimitRule { PerSeconds = 60, MaxRequests = 600 }], Microservices = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["scanner"] = new MicroserviceLimitsConfig { Rules = [new RateLimitRule { PerSeconds = 60, MaxRequests = 600 }], Routes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["scan_submit"] = new RouteLimitsConfig { Pattern = "/api/scans", MatchType = RouteMatchType.Exact, Rules = [new RateLimitRule { PerSeconds = 300, MaxRequests = 1 }] } } } } } }.Validate(); var resolver = new LimitInheritanceResolver(config); var target = resolver.ResolveEnvironmentTarget("scanner", "/api/scans"); target.Enabled.Should().BeTrue(); target.Kind.Should().Be(RateLimitTargetKind.Route); target.RouteName.Should().Be("scan_submit"); target.TargetKey.Should().Be("scanner:scan_submit"); target.Rules.Should().ContainSingle(); target.Rules[0].PerSeconds.Should().Be(300); target.Rules[0].MaxRequests.Should().Be(1); } [Fact] public void ResolveEnvironmentTarget_DoesNotTreatRouteAsOverrideWhenItHasNoRules() { var config = new RateLimitConfig { ActivationThresholdPer5Min = 0, ForEnvironment = new EnvironmentLimitsConfig { ValkeyConnection = "localhost:6379", ValkeyBucket = "bucket", Rules = [new RateLimitRule { PerSeconds = 60, MaxRequests = 600 }], Microservices = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["scanner"] = new MicroserviceLimitsConfig { Rules = [new RateLimitRule { PerSeconds = 60, MaxRequests = 600 }], Routes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["named_only"] = new RouteLimitsConfig { Pattern = "/api/scans", MatchType = RouteMatchType.Exact } } } } } }.Validate(); var resolver = new LimitInheritanceResolver(config); var target = resolver.ResolveEnvironmentTarget("scanner", "/api/scans"); target.Enabled.Should().BeTrue(); target.Kind.Should().Be(RateLimitTargetKind.Microservice); target.RouteName.Should().BeNull(); target.TargetKey.Should().Be("scanner"); } }