From ec6d3d57faef10a0f38a397b186d05c0df314465 Mon Sep 17 00:00:00 2001 From: Andreas Frisch Date: Wed, 5 Aug 2026 18:11:45 +0200 Subject: [PATCH] rogue-capabilities-compliance-data-endpoints --- .../TestComplianceApplicationService.cs | 54 ++++++++++++++++++ .../ComplianceApplicationService.cs | 55 ++++++++++++++----- .../IComplianceApplicationService.cs | 2 + .../StubComplianceApplicationService.cs | 55 ++++++++++++++----- .../Api/Compliance/ComplianceController.cs | 14 +++++ 5 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/SelfService.Tests/Application/TestComplianceApplicationService.cs b/src/SelfService.Tests/Application/TestComplianceApplicationService.cs index 6865878a..f24223a1 100644 --- a/src/SelfService.Tests/Application/TestComplianceApplicationService.cs +++ b/src/SelfService.Tests/Application/TestComplianceApplicationService.cs @@ -457,4 +457,58 @@ public async Task GetCapabilityCompliance_TagWithEmptyValue_TreatedAsMissing() var costCentreItem = tagsCategory.Items.First(i => i.Name == "dfds.cost.centre"); Assert.Equal("missing", costCentreItem.Status); } + + [Fact] + public async Task GetRogueCapabilitiesCompliance_FiltersCapabilitiesWithoutCostCentre() + { + var withCostCentre = A + .Capability.WithId(CapabilityId.CreateFrom("cost-centre-cap")) + .WithJsonMetadata(AllTagsPresent) + .Build(); + var missingCostCentre = A + .Capability.WithId(CapabilityId.CreateFrom("missing-cost-centre-cap")) + .WithJsonMetadata("""{"dfds.businessCapability": "Platform"}""") + .Build(); + var emptyCostCentre = A + .Capability.WithId(CapabilityId.CreateFrom("empty-cost-centre-cap")) + .WithJsonMetadata("""{"dfds.cost.centre": ""}""") + .Build(); + + var repo = new Mock(); + repo.Setup(r => r.GetAllActive()).ReturnsAsync(new[] { withCostCentre, missingCostCentre, emptyCostCentre }); + + var service = A.ComplianceApplicationService.WithCapabilityRepository(repo.Object).Build(); + + var result = await service.GetRogueCapabilitiesCompliance(); + + Assert.Equal("rogue", result.CostCentre); + Assert.Equal(2, result.TotalCapabilities); + } + + [Fact] + public async Task GetRogueCapabilitiesComplianceDetails_MatchesAggregateCounts() + { + var rogueCap = A + .Capability.WithId(CapabilityId.CreateFrom("rogue-cap")) + .WithJsonMetadata("""{"dfds.businessCapability": "Platform"}""") + .Build(); + var nonRogueCap = A + .Capability.WithId(CapabilityId.CreateFrom("non-rogue-cap")) + .WithJsonMetadata(AllTagsPresent) + .Build(); + + var repo = new Mock(); + repo.Setup(r => r.GetAllActive()).ReturnsAsync(new[] { rogueCap, nonRogueCap }); + + var service = A.ComplianceApplicationService.WithCapabilityRepository(repo.Object).Build(); + + var details = await service.GetRogueCapabilitiesComplianceDetails(); + var aggregate = await service.GetRogueCapabilitiesCompliance(); + + Assert.Equal("rogue", details.CostCentre); + Assert.Single(details.Capabilities); + Assert.Equal(aggregate.TotalCapabilities, details.TotalCapabilities); + Assert.Equal(aggregate.CompliantCount, details.CompliantCount); + Assert.Equal(aggregate.NonCompliantCount, details.NonCompliantCount); + } } diff --git a/src/SelfService/Application/ComplianceApplicationService.cs b/src/SelfService/Application/ComplianceApplicationService.cs index 3cbe929d..2d12eb34 100644 --- a/src/SelfService/Application/ComplianceApplicationService.cs +++ b/src/SelfService/Application/ComplianceApplicationService.cs @@ -15,6 +15,7 @@ public class ComplianceApplicationService : IComplianceApplicationService private readonly RequirementsDbContext _requirementsDbContext; private static readonly string[] PlaceholderCategories = Array.Empty(); + private const string RogueCostCentreName = "rogue"; private static readonly string[] Categories = { @@ -79,25 +80,39 @@ private async Task HasKubernetesContext(CapabilityId capabilityId) public async Task GetCostCentreCompliance(string costCentre) { var details = await GetCostCentreComplianceDetails(costCentre); - return new CostCentreComplianceResult - { - CostCentre = details.CostCentre, - TotalCapabilities = details.TotalCapabilities, - CompliantCount = details.CompliantCount, - NonCompliantCount = details.NonCompliantCount, - Categories = details.Categories, - }; + return ToCostCentreComplianceResult(details); } public async Task GetCostCentreComplianceDetails(string costCentre) + { + return await BuildComplianceDetailsForCapabilities( + costCentre, + c => string.Equals(ExtractCostCentre(c.JsonMetadata), costCentre, StringComparison.OrdinalIgnoreCase) + ); + } + + public async Task GetRogueCapabilitiesCompliance() + { + var details = await GetRogueCapabilitiesComplianceDetails(); + return ToCostCentreComplianceResult(details); + } + + public async Task GetRogueCapabilitiesComplianceDetails() + { + return await BuildComplianceDetailsForCapabilities( + RogueCostCentreName, + c => string.IsNullOrWhiteSpace(ExtractCostCentre(c.JsonMetadata)) + ); + } + + private async Task BuildComplianceDetailsForCapabilities( + string costCentreLabel, + Func filter + ) { var activeCapabilities = await _capabilityRepository.GetAllActive(); - var matchingCapabilities = activeCapabilities - .Where(c => - string.Equals(ExtractCostCentre(c.JsonMetadata), costCentre, StringComparison.OrdinalIgnoreCase) - ) - .ToList(); + var matchingCapabilities = activeCapabilities.Where(filter).ToList(); var capabilityIds = matchingCapabilities.Select(c => c.Id.ToString()).ToList(); var allMetrics = await _requirementsDbContext @@ -161,7 +176,7 @@ public async Task GetCostCentreComplianceDeta return new CostCentreComplianceDetailsResult { - CostCentre = costCentre, + CostCentre = costCentreLabel, TotalCapabilities = matchingCapabilities.Count, CompliantCount = capabilityResults.Count(r => r.OverallStatus == ComplianceStatus.Compliant), NonCompliantCount = capabilityResults.Count(r => r.OverallStatus == ComplianceStatus.NonCompliant), @@ -171,6 +186,18 @@ public async Task GetCostCentreComplianceDeta }; } + private static CostCentreComplianceResult ToCostCentreComplianceResult(CostCentreComplianceDetailsResult details) + { + return new CostCentreComplianceResult + { + CostCentre = details.CostCentre, + TotalCapabilities = details.TotalCapabilities, + CompliantCount = details.CompliantCount, + NonCompliantCount = details.NonCompliantCount, + Categories = details.Categories, + }; + } + private static ComplianceCategoryResult CheckTagCompliance(string? jsonMetadata) { var evaluation = TagComplianceEvaluator.Evaluate(jsonMetadata); diff --git a/src/SelfService/Application/IComplianceApplicationService.cs b/src/SelfService/Application/IComplianceApplicationService.cs index dff9dfd2..aff8a045 100644 --- a/src/SelfService/Application/IComplianceApplicationService.cs +++ b/src/SelfService/Application/IComplianceApplicationService.cs @@ -7,4 +7,6 @@ public interface IComplianceApplicationService Task GetCapabilityCompliance(CapabilityId capabilityId); Task GetCostCentreCompliance(string costCentre); Task GetCostCentreComplianceDetails(string costCentre); + Task GetRogueCapabilitiesCompliance(); + Task GetRogueCapabilitiesComplianceDetails(); } diff --git a/src/SelfService/Application/StubComplianceApplicationService.cs b/src/SelfService/Application/StubComplianceApplicationService.cs index e1edfc0d..5b0a75d8 100644 --- a/src/SelfService/Application/StubComplianceApplicationService.cs +++ b/src/SelfService/Application/StubComplianceApplicationService.cs @@ -11,6 +11,7 @@ public class StubComplianceApplicationService : IComplianceApplicationService private readonly IAwsAccountRepository _awsAccountRepository; private static readonly string[] PlaceholderCategories = Array.Empty(); + private const string RogueCostCentreName = "rogue"; public StubComplianceApplicationService( ICapabilityRepository capabilityRepository, @@ -108,25 +109,39 @@ public async Task GetCapabilityCompliance(Capability public async Task GetCostCentreCompliance(string costCentre) { var details = await GetCostCentreComplianceDetails(costCentre); - return new CostCentreComplianceResult - { - CostCentre = details.CostCentre, - TotalCapabilities = details.TotalCapabilities, - CompliantCount = details.CompliantCount, - NonCompliantCount = details.NonCompliantCount, - Categories = details.Categories, - }; + return ToCostCentreComplianceResult(details); } public async Task GetCostCentreComplianceDetails(string costCentre) + { + return await BuildComplianceDetailsForCapabilities( + costCentre, + c => string.Equals(ExtractCostCentre(c.JsonMetadata), costCentre, StringComparison.OrdinalIgnoreCase) + ); + } + + public async Task GetRogueCapabilitiesCompliance() + { + var details = await GetRogueCapabilitiesComplianceDetails(); + return ToCostCentreComplianceResult(details); + } + + public async Task GetRogueCapabilitiesComplianceDetails() + { + return await BuildComplianceDetailsForCapabilities( + RogueCostCentreName, + c => string.IsNullOrWhiteSpace(ExtractCostCentre(c.JsonMetadata)) + ); + } + + private async Task BuildComplianceDetailsForCapabilities( + string costCentreLabel, + Func filter + ) { var activeCapabilities = await _capabilityRepository.GetAllActive(); - var matchingCapabilities = activeCapabilities - .Where(c => - string.Equals(ExtractCostCentre(c.JsonMetadata), costCentre, StringComparison.OrdinalIgnoreCase) - ) - .ToList(); + var matchingCapabilities = activeCapabilities.Where(filter).ToList(); var awsAccounts = await _awsAccountRepository.GetByCapabilityIds(matchingCapabilities.Select(c => c.Id)); var k8sCapabilityIds = awsAccounts @@ -216,7 +231,7 @@ public async Task GetCostCentreComplianceDeta return new CostCentreComplianceDetailsResult { - CostCentre = costCentre, + CostCentre = costCentreLabel, TotalCapabilities = matchingCapabilities.Count, CompliantCount = capabilityResults.Count(r => r.OverallStatus == ComplianceStatus.Compliant), NonCompliantCount = capabilityResults.Count(r => r.OverallStatus == ComplianceStatus.NonCompliant), @@ -226,6 +241,18 @@ public async Task GetCostCentreComplianceDeta }; } + private static CostCentreComplianceResult ToCostCentreComplianceResult(CostCentreComplianceDetailsResult details) + { + return new CostCentreComplianceResult + { + CostCentre = details.CostCentre, + TotalCapabilities = details.TotalCapabilities, + CompliantCount = details.CompliantCount, + NonCompliantCount = details.NonCompliantCount, + Categories = details.Categories, + }; + } + private async Task HasKubernetesContext(CapabilityId capabilityId) { var awsAccount = await _awsAccountRepository.FindBy(capabilityId); diff --git a/src/SelfService/Infrastructure/Api/Compliance/ComplianceController.cs b/src/SelfService/Infrastructure/Api/Compliance/ComplianceController.cs index ed776167..02acbf33 100644 --- a/src/SelfService/Infrastructure/Api/Compliance/ComplianceController.cs +++ b/src/SelfService/Infrastructure/Api/Compliance/ComplianceController.cs @@ -45,4 +45,18 @@ public async Task GetCostCentreComplianceDetails([FromRoute] stri var result = await _complianceService.GetCostCentreComplianceDetails(costCentre); return Ok(CostCentreComplianceDetailsApiResource.From(result)); } + + [HttpGet("rogue-capabilities")] + public async Task GetRogueCapabilitiesCompliance() + { + var result = await _complianceService.GetRogueCapabilitiesCompliance(); + return Ok(CostCentreComplianceApiResource.From(result)); + } + + [HttpGet("rogue-capabilities/details")] + public async Task GetRogueCapabilitiesComplianceDetails() + { + var result = await _complianceService.GetRogueCapabilitiesComplianceDetails(); + return Ok(CostCentreComplianceDetailsApiResource.From(result)); + } }