Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -0,0 +1,870 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20251229_021a_FE_policy_governance_controls
|
||||
// Task: GOV-018 - Sealed mode overrides and risk profile events endpoints
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Policy governance API endpoints for sealed mode and risk profile management.
|
||||
/// </summary>
|
||||
public static class GovernanceEndpoints
|
||||
{
|
||||
// In-memory stores for development
|
||||
private static readonly ConcurrentDictionary<string, SealedModeState> SealedModeStates = new();
|
||||
private static readonly ConcurrentDictionary<string, SealedModeOverrideEntity> Overrides = new();
|
||||
private static readonly ConcurrentDictionary<string, RiskProfileEntity> RiskProfiles = new();
|
||||
private static readonly ConcurrentDictionary<string, GovernanceAuditEntry> AuditEntries = new();
|
||||
|
||||
/// <summary>
|
||||
/// Maps governance endpoints to the application.
|
||||
/// </summary>
|
||||
public static void MapGovernanceEndpoints(this WebApplication app)
|
||||
{
|
||||
var governance = app.MapGroup("/api/v1/governance")
|
||||
.WithTags("Governance");
|
||||
|
||||
// Sealed Mode endpoints
|
||||
governance.MapGet("/sealed-mode/status", GetSealedModeStatusAsync)
|
||||
.WithName("GetSealedModeStatus")
|
||||
.WithDescription("Get sealed mode status");
|
||||
|
||||
governance.MapGet("/sealed-mode/overrides", GetSealedModeOverridesAsync)
|
||||
.WithName("GetSealedModeOverrides")
|
||||
.WithDescription("List sealed mode overrides");
|
||||
|
||||
governance.MapPost("/sealed-mode/toggle", ToggleSealedModeAsync)
|
||||
.WithName("ToggleSealedMode")
|
||||
.WithDescription("Toggle sealed mode on/off");
|
||||
|
||||
governance.MapPost("/sealed-mode/overrides", CreateSealedModeOverrideAsync)
|
||||
.WithName("CreateSealedModeOverride")
|
||||
.WithDescription("Create a sealed mode override");
|
||||
|
||||
governance.MapPost("/sealed-mode/overrides/{overrideId}/revoke", RevokeSealedModeOverrideAsync)
|
||||
.WithName("RevokeSealedModeOverride")
|
||||
.WithDescription("Revoke a sealed mode override");
|
||||
|
||||
// Risk Profile endpoints
|
||||
governance.MapGet("/risk-profiles", ListRiskProfilesAsync)
|
||||
.WithName("ListRiskProfiles")
|
||||
.WithDescription("List risk profiles");
|
||||
|
||||
governance.MapGet("/risk-profiles/{profileId}", GetRiskProfileAsync)
|
||||
.WithName("GetRiskProfile")
|
||||
.WithDescription("Get a risk profile by ID");
|
||||
|
||||
governance.MapPost("/risk-profiles", CreateRiskProfileAsync)
|
||||
.WithName("CreateRiskProfile")
|
||||
.WithDescription("Create a new risk profile");
|
||||
|
||||
governance.MapPut("/risk-profiles/{profileId}", UpdateRiskProfileAsync)
|
||||
.WithName("UpdateRiskProfile")
|
||||
.WithDescription("Update a risk profile");
|
||||
|
||||
governance.MapDelete("/risk-profiles/{profileId}", DeleteRiskProfileAsync)
|
||||
.WithName("DeleteRiskProfile")
|
||||
.WithDescription("Delete a risk profile");
|
||||
|
||||
governance.MapPost("/risk-profiles/{profileId}/activate", ActivateRiskProfileAsync)
|
||||
.WithName("ActivateRiskProfile")
|
||||
.WithDescription("Activate a risk profile");
|
||||
|
||||
governance.MapPost("/risk-profiles/{profileId}/deprecate", DeprecateRiskProfileAsync)
|
||||
.WithName("DeprecateRiskProfile")
|
||||
.WithDescription("Deprecate a risk profile");
|
||||
|
||||
governance.MapPost("/risk-profiles/validate", ValidateRiskProfileAsync)
|
||||
.WithName("ValidateRiskProfile")
|
||||
.WithDescription("Validate a risk profile");
|
||||
|
||||
// Audit endpoints
|
||||
governance.MapGet("/audit/events", GetAuditEventsAsync)
|
||||
.WithName("GetGovernanceAuditEvents")
|
||||
.WithDescription("Get governance audit events");
|
||||
|
||||
governance.MapGet("/audit/events/{eventId}", GetAuditEventAsync)
|
||||
.WithName("GetGovernanceAuditEvent")
|
||||
.WithDescription("Get a specific audit event");
|
||||
|
||||
// Initialize default profiles
|
||||
InitializeDefaultProfiles();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Sealed Mode Handlers
|
||||
// ========================================================================
|
||||
|
||||
private static Task<IResult> GetSealedModeStatusAsync(
|
||||
HttpContext httpContext,
|
||||
[FromQuery] string? tenantId)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
IsSealed = state.IsSealed,
|
||||
SealedAt = state.SealedAt,
|
||||
SealedBy = state.SealedBy,
|
||||
Reason = state.Reason,
|
||||
TrustRoots = state.TrustRoots,
|
||||
AllowedSources = state.AllowedSources,
|
||||
Overrides = Overrides.Values
|
||||
.Where(o => o.TenantId == tenant && o.Active)
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList(),
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = DateTimeOffset.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
private static Task<IResult> GetSealedModeOverridesAsync(
|
||||
HttpContext httpContext,
|
||||
[FromQuery] string? tenantId)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
|
||||
var overrides = Overrides.Values
|
||||
.Where(o => o.TenantId == tenant)
|
||||
.Select(MapOverrideToResponse)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(Results.Ok(overrides));
|
||||
}
|
||||
|
||||
private static Task<IResult> ToggleSealedModeAsync(
|
||||
HttpContext httpContext,
|
||||
SealedModeToggleRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var state = SealedModeStates.GetOrAdd(tenant, _ => new SealedModeState());
|
||||
|
||||
if (request.Enable)
|
||||
{
|
||||
state = new SealedModeState
|
||||
{
|
||||
IsSealed = true,
|
||||
SealedAt = now.ToString("O"),
|
||||
SealedBy = actor,
|
||||
Reason = request.Reason,
|
||||
TrustRoots = request.TrustRoots ?? [],
|
||||
AllowedSources = request.AllowedSources ?? []
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
state = new SealedModeState
|
||||
{
|
||||
IsSealed = false,
|
||||
LastUnsealedAt = now.ToString("O")
|
||||
};
|
||||
}
|
||||
|
||||
SealedModeStates[tenant] = state;
|
||||
|
||||
// Audit
|
||||
RecordAudit(tenant, actor, "sealed_mode_toggled", "sealed-mode", "system_config",
|
||||
$"{(request.Enable ? "Enabled" : "Disabled")} sealed mode: {request.Reason}");
|
||||
|
||||
var response = new SealedModeStatusResponse
|
||||
{
|
||||
IsSealed = state.IsSealed,
|
||||
SealedAt = state.SealedAt,
|
||||
SealedBy = state.SealedBy,
|
||||
Reason = state.Reason,
|
||||
TrustRoots = state.TrustRoots,
|
||||
AllowedSources = state.AllowedSources,
|
||||
Overrides = [],
|
||||
VerificationStatus = "verified",
|
||||
LastVerifiedAt = now.ToString("O")
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
private static Task<IResult> CreateSealedModeOverrideAsync(
|
||||
HttpContext httpContext,
|
||||
SealedModeOverrideRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var overrideId = $"override-{Guid.NewGuid():N}";
|
||||
var entity = new SealedModeOverrideEntity
|
||||
{
|
||||
Id = overrideId,
|
||||
TenantId = tenant,
|
||||
Type = request.Type,
|
||||
Target = request.Target,
|
||||
Reason = request.Reason,
|
||||
ApprovalId = $"approval-{Guid.NewGuid():N}",
|
||||
ApprovedBy = [actor],
|
||||
ExpiresAt = now.AddHours(request.DurationHours).ToString("O"),
|
||||
CreatedAt = now.ToString("O"),
|
||||
Active = true
|
||||
};
|
||||
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_created", overrideId, "sealed_mode_override",
|
||||
$"Created override for {request.Target}: {request.Reason}");
|
||||
|
||||
return Task.FromResult(Results.Ok(MapOverrideToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> RevokeSealedModeOverrideAsync(
|
||||
HttpContext httpContext,
|
||||
string overrideId,
|
||||
RevokeOverrideRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
|
||||
if (!Overrides.TryGetValue(overrideId, out var entity) || entity.TenantId != tenant)
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Override not found",
|
||||
Status = 404
|
||||
}));
|
||||
}
|
||||
|
||||
entity.Active = false;
|
||||
Overrides[overrideId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "sealed_mode_override_revoked", overrideId, "sealed_mode_override",
|
||||
$"Revoked override: {request.Reason}");
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Risk Profile Handlers
|
||||
// ========================================================================
|
||||
|
||||
private static Task<IResult> ListRiskProfilesAsync(
|
||||
HttpContext httpContext,
|
||||
[FromQuery] string? tenantId,
|
||||
[FromQuery] string? status)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
|
||||
var profiles = RiskProfiles.Values
|
||||
.Where(p => p.TenantId == tenant || p.TenantId == "default")
|
||||
.Where(p => string.IsNullOrEmpty(status) || p.Status.Equals(status, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(MapProfileToResponse)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(Results.Ok(profiles));
|
||||
}
|
||||
|
||||
private static Task<IResult> GetRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var profile))
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Profile not found",
|
||||
Status = 404,
|
||||
Detail = $"Risk profile '{profileId}' not found."
|
||||
}));
|
||||
}
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(profile)));
|
||||
}
|
||||
|
||||
private static Task<IResult> CreateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
CreateRiskProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var profileId = $"profile-{Guid.NewGuid():N}";
|
||||
var entity = new RiskProfileEntity
|
||||
{
|
||||
Id = profileId,
|
||||
TenantId = tenant,
|
||||
Version = "1.0.0",
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Status = "draft",
|
||||
ExtendsProfile = request.ExtendsProfile,
|
||||
Signals = request.Signals ?? [],
|
||||
SeverityOverrides = request.SeverityOverrides ?? [],
|
||||
ActionOverrides = request.ActionOverrides ?? [],
|
||||
CreatedAt = now.ToString("O"),
|
||||
ModifiedAt = now.ToString("O"),
|
||||
CreatedBy = actor,
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_created", profileId, "risk_profile",
|
||||
$"Created risk profile: {request.Name}");
|
||||
|
||||
return Task.FromResult(Results.Created($"/api/v1/governance/risk-profiles/{profileId}", MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> UpdateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
string profileId,
|
||||
UpdateRiskProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Profile not found",
|
||||
Status = 404
|
||||
}));
|
||||
}
|
||||
|
||||
var entity = existing with
|
||||
{
|
||||
Name = request.Name ?? existing.Name,
|
||||
Description = request.Description ?? existing.Description,
|
||||
Signals = request.Signals ?? existing.Signals,
|
||||
SeverityOverrides = request.SeverityOverrides ?? existing.SeverityOverrides,
|
||||
ActionOverrides = request.ActionOverrides ?? existing.ActionOverrides,
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_updated", profileId, "risk_profile",
|
||||
$"Updated risk profile: {entity.Name}");
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> DeleteRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
|
||||
if (!RiskProfiles.TryRemove(profileId, out var removed))
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Profile not found",
|
||||
Status = 404
|
||||
}));
|
||||
}
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deleted", profileId, "risk_profile",
|
||||
$"Deleted risk profile: {removed.Name}");
|
||||
|
||||
return Task.FromResult(Results.NoContent());
|
||||
}
|
||||
|
||||
private static Task<IResult> ActivateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
string profileId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Profile not found",
|
||||
Status = 404
|
||||
}));
|
||||
}
|
||||
|
||||
var entity = existing with
|
||||
{
|
||||
Status = "active",
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedBy = actor
|
||||
};
|
||||
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_activated", profileId, "risk_profile",
|
||||
$"Activated risk profile: {entity.Name}");
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> DeprecateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
string profileId,
|
||||
DeprecateProfileRequest request)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
var actor = GetActorId(httpContext) ?? "system";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!RiskProfiles.TryGetValue(profileId, out var existing))
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Profile not found",
|
||||
Status = 404
|
||||
}));
|
||||
}
|
||||
|
||||
var entity = existing with
|
||||
{
|
||||
Status = "deprecated",
|
||||
ModifiedAt = now.ToString("O"),
|
||||
ModifiedBy = actor,
|
||||
DeprecationReason = request.Reason
|
||||
};
|
||||
|
||||
RiskProfiles[profileId] = entity;
|
||||
|
||||
RecordAudit(tenant, actor, "risk_profile_deprecated", profileId, "risk_profile",
|
||||
$"Deprecated risk profile: {entity.Name} - {request.Reason}");
|
||||
|
||||
return Task.FromResult(Results.Ok(MapProfileToResponse(entity)));
|
||||
}
|
||||
|
||||
private static Task<IResult> ValidateRiskProfileAsync(
|
||||
HttpContext httpContext,
|
||||
ValidateRiskProfileRequest request)
|
||||
{
|
||||
var errors = new List<ValidationError>();
|
||||
var warnings = new List<ValidationWarning>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
errors.Add(new ValidationError("MISSING_NAME", "Profile name is required", "name"));
|
||||
}
|
||||
|
||||
if (request.Signals == null || request.Signals.Count == 0)
|
||||
{
|
||||
errors.Add(new ValidationError("NO_SIGNALS", "At least one signal must be defined", "signals"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var totalWeight = request.Signals.Where(s => s.Enabled).Sum(s => s.Weight);
|
||||
if (Math.Abs(totalWeight - 1.0) > 0.01)
|
||||
{
|
||||
warnings.Add(new ValidationWarning("WEIGHT_SUM", $"Signal weights sum to {totalWeight:F2}, expected 1.0", "signals"));
|
||||
}
|
||||
}
|
||||
|
||||
var response = new RiskProfileValidationResponse
|
||||
{
|
||||
Valid = errors.Count == 0,
|
||||
Errors = errors,
|
||||
Warnings = warnings
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Audit Handlers
|
||||
// ========================================================================
|
||||
|
||||
private static Task<IResult> GetAuditEventsAsync(
|
||||
HttpContext httpContext,
|
||||
[FromQuery] string? tenantId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
var tenant = tenantId ?? GetTenantId(httpContext) ?? "default";
|
||||
|
||||
var events = AuditEntries.Values
|
||||
.Where(e => e.TenantId == tenant)
|
||||
.OrderByDescending(e => e.Timestamp)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(MapAuditToResponse)
|
||||
.ToList();
|
||||
|
||||
var total = AuditEntries.Values.Count(e => e.TenantId == tenant);
|
||||
|
||||
var response = new AuditEventsResponse
|
||||
{
|
||||
Events = events,
|
||||
Total = total,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
HasMore = (page * pageSize) < total
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
private static Task<IResult> GetAuditEventAsync(
|
||||
HttpContext httpContext,
|
||||
string eventId)
|
||||
{
|
||||
var tenant = GetTenantId(httpContext) ?? "default";
|
||||
|
||||
if (!AuditEntries.TryGetValue(eventId, out var entry) || entry.TenantId != tenant)
|
||||
{
|
||||
return Task.FromResult(Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Event not found",
|
||||
Status = 404
|
||||
}));
|
||||
}
|
||||
|
||||
return Task.FromResult(Results.Ok(MapAuditToResponse(entry)));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Helper Methods
|
||||
// ========================================================================
|
||||
|
||||
private static void InitializeDefaultProfiles()
|
||||
{
|
||||
if (RiskProfiles.IsEmpty)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToString("O");
|
||||
RiskProfiles["profile-default"] = new RiskProfileEntity
|
||||
{
|
||||
Id = "profile-default",
|
||||
TenantId = "default",
|
||||
Version = "1.0.0",
|
||||
Name = "Default Risk Profile",
|
||||
Description = "Standard risk evaluation profile",
|
||||
Status = "active",
|
||||
Signals = [
|
||||
new RiskSignal { Name = "cvss_score", Weight = 0.3, Description = "CVSS base score", Enabled = true },
|
||||
new RiskSignal { Name = "exploit_available", Weight = 0.25, Description = "Known exploit exists", Enabled = true },
|
||||
new RiskSignal { Name = "reachability", Weight = 0.2, Description = "Code reachability", Enabled = true },
|
||||
new RiskSignal { Name = "asset_criticality", Weight = 0.15, Description = "Asset business criticality", Enabled = true },
|
||||
new RiskSignal { Name = "patch_available", Weight = 0.1, Description = "Patch availability", Enabled = true }
|
||||
],
|
||||
SeverityOverrides = [],
|
||||
ActionOverrides = [],
|
||||
CreatedAt = now,
|
||||
ModifiedAt = now,
|
||||
CreatedBy = "system",
|
||||
ModifiedBy = "system"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetTenantId(HttpContext httpContext)
|
||||
{
|
||||
return httpContext.User.Claims.FirstOrDefault(c => c.Type == "tenant_id")?.Value
|
||||
?? httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault()
|
||||
?? httpContext.Request.Headers["X-StellaOps-Tenant"].FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string? GetActorId(HttpContext httpContext)
|
||||
{
|
||||
return httpContext.User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value
|
||||
?? httpContext.User.Identity?.Name
|
||||
?? httpContext.Request.Headers["X-StellaOps-Actor"].FirstOrDefault();
|
||||
}
|
||||
|
||||
private static void RecordAudit(string tenantId, string actor, string eventType, string targetId, string targetType, string summary)
|
||||
{
|
||||
var id = $"audit-{Guid.NewGuid():N}";
|
||||
AuditEntries[id] = new GovernanceAuditEntry
|
||||
{
|
||||
Id = id,
|
||||
TenantId = tenantId,
|
||||
Type = eventType,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
Actor = actor,
|
||||
ActorType = "user",
|
||||
TargetResource = targetId,
|
||||
TargetResourceType = targetType,
|
||||
Summary = summary
|
||||
};
|
||||
}
|
||||
|
||||
private static SealedModeOverrideResponse MapOverrideToResponse(SealedModeOverrideEntity entity)
|
||||
{
|
||||
return new SealedModeOverrideResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
Type = entity.Type,
|
||||
Target = entity.Target,
|
||||
Reason = entity.Reason,
|
||||
ApprovalId = entity.ApprovalId,
|
||||
ApprovedBy = entity.ApprovedBy,
|
||||
ExpiresAt = entity.ExpiresAt,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
Active = entity.Active
|
||||
};
|
||||
}
|
||||
|
||||
private static RiskProfileResponse MapProfileToResponse(RiskProfileEntity entity)
|
||||
{
|
||||
return new RiskProfileResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
Version = entity.Version,
|
||||
Name = entity.Name,
|
||||
Description = entity.Description,
|
||||
Status = entity.Status,
|
||||
ExtendsProfile = entity.ExtendsProfile,
|
||||
Signals = entity.Signals,
|
||||
SeverityOverrides = entity.SeverityOverrides,
|
||||
ActionOverrides = entity.ActionOverrides,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
ModifiedAt = entity.ModifiedAt,
|
||||
CreatedBy = entity.CreatedBy,
|
||||
ModifiedBy = entity.ModifiedBy
|
||||
};
|
||||
}
|
||||
|
||||
private static GovernanceAuditEventResponse MapAuditToResponse(GovernanceAuditEntry entry)
|
||||
{
|
||||
return new GovernanceAuditEventResponse
|
||||
{
|
||||
Id = entry.Id,
|
||||
Type = entry.Type,
|
||||
Timestamp = entry.Timestamp,
|
||||
Actor = entry.Actor,
|
||||
ActorType = entry.ActorType,
|
||||
TargetResource = entry.TargetResource,
|
||||
TargetResourceType = entry.TargetResourceType,
|
||||
Summary = entry.Summary,
|
||||
TenantId = entry.TenantId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal Entities
|
||||
// ============================================================================
|
||||
|
||||
internal sealed class SealedModeState
|
||||
{
|
||||
public bool IsSealed { get; set; }
|
||||
public string? SealedAt { get; set; }
|
||||
public string? SealedBy { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public string? LastUnsealedAt { get; set; }
|
||||
public List<string> TrustRoots { get; set; } = [];
|
||||
public List<string> AllowedSources { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed record SealedModeOverrideEntity
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Target { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public required string ApprovalId { get; init; }
|
||||
public required List<string> ApprovedBy { get; init; }
|
||||
public required string ExpiresAt { get; init; }
|
||||
public required string CreatedAt { get; init; }
|
||||
public bool Active { get; set; }
|
||||
}
|
||||
|
||||
internal sealed record RiskProfileEntity
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public string? ExtendsProfile { get; init; }
|
||||
public required List<RiskSignal> Signals { get; init; }
|
||||
public required List<SeverityOverride> SeverityOverrides { get; init; }
|
||||
public required List<ActionOverride> ActionOverrides { get; init; }
|
||||
public required string CreatedAt { get; init; }
|
||||
public required string ModifiedAt { get; init; }
|
||||
public required string CreatedBy { get; init; }
|
||||
public required string ModifiedBy { get; init; }
|
||||
public string? DeprecationReason { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record GovernanceAuditEntry
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Timestamp { get; init; }
|
||||
public required string Actor { get; init; }
|
||||
public required string ActorType { get; init; }
|
||||
public required string TargetResource { get; init; }
|
||||
public required string TargetResourceType { get; init; }
|
||||
public required string Summary { get; init; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Request/Response DTOs
|
||||
// ============================================================================
|
||||
|
||||
public sealed record SealedModeToggleRequest
|
||||
{
|
||||
public required bool Enable { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public List<string>? TrustRoots { get; init; }
|
||||
public List<string>? AllowedSources { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SealedModeOverrideRequest
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required string Target { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public int DurationHours { get; init; } = 24;
|
||||
}
|
||||
|
||||
public sealed record RevokeOverrideRequest
|
||||
{
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SealedModeStatusResponse
|
||||
{
|
||||
public required bool IsSealed { get; init; }
|
||||
public string? SealedAt { get; init; }
|
||||
public string? SealedBy { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public required List<string> TrustRoots { get; init; }
|
||||
public required List<string> AllowedSources { get; init; }
|
||||
public required List<SealedModeOverrideResponse> Overrides { get; init; }
|
||||
public required string VerificationStatus { get; init; }
|
||||
public string? LastVerifiedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SealedModeOverrideResponse
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Target { get; init; }
|
||||
public required string Reason { get; init; }
|
||||
public required string ApprovalId { get; init; }
|
||||
public required List<string> ApprovedBy { get; init; }
|
||||
public required string ExpiresAt { get; init; }
|
||||
public required string CreatedAt { get; init; }
|
||||
public required bool Active { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CreateRiskProfileRequest
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? ExtendsProfile { get; init; }
|
||||
public List<RiskSignal>? Signals { get; init; }
|
||||
public List<SeverityOverride>? SeverityOverrides { get; init; }
|
||||
public List<ActionOverride>? ActionOverrides { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UpdateRiskProfileRequest
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public List<RiskSignal>? Signals { get; init; }
|
||||
public List<SeverityOverride>? SeverityOverrides { get; init; }
|
||||
public List<ActionOverride>? ActionOverrides { get; init; }
|
||||
}
|
||||
|
||||
public sealed record DeprecateProfileRequest
|
||||
{
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ValidateRiskProfileRequest
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public List<RiskSignal>? Signals { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RiskProfileResponse
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public string? ExtendsProfile { get; init; }
|
||||
public required List<RiskSignal> Signals { get; init; }
|
||||
public required List<SeverityOverride> SeverityOverrides { get; init; }
|
||||
public required List<ActionOverride> ActionOverrides { get; init; }
|
||||
public required string CreatedAt { get; init; }
|
||||
public required string ModifiedAt { get; init; }
|
||||
public required string CreatedBy { get; init; }
|
||||
public required string ModifiedBy { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RiskProfileValidationResponse
|
||||
{
|
||||
public required bool Valid { get; init; }
|
||||
public required List<ValidationError> Errors { get; init; }
|
||||
public required List<ValidationWarning> Warnings { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ValidationError(string Code, string Message, string? Path = null);
|
||||
public sealed record ValidationWarning(string Code, string Message, string? Path = null);
|
||||
|
||||
public sealed record RiskSignal
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required double Weight { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
public sealed record SeverityOverride
|
||||
{
|
||||
public string? Id { get; init; }
|
||||
public string? TargetSeverity { get; init; }
|
||||
public object? Condition { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public int Priority { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ActionOverride
|
||||
{
|
||||
public string? Id { get; init; }
|
||||
public string? TargetAction { get; init; }
|
||||
public object? Condition { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public int Priority { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AuditEventsResponse
|
||||
{
|
||||
public required List<GovernanceAuditEventResponse> Events { get; init; }
|
||||
public required int Total { get; init; }
|
||||
public required int Page { get; init; }
|
||||
public required int PageSize { get; init; }
|
||||
public required bool HasMore { get; init; }
|
||||
}
|
||||
|
||||
public sealed record GovernanceAuditEventResponse
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Timestamp { get; init; }
|
||||
public required string Actor { get; init; }
|
||||
public required string ActorType { get; init; }
|
||||
public required string TargetResource { get; init; }
|
||||
public required string TargetResourceType { get; init; }
|
||||
public required string Summary { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
}
|
||||
@@ -530,6 +530,9 @@ app.MapRegistryWebhooks();
|
||||
// Exception approval endpoints (Sprint: SPRINT_20251226_003_BE_exception_approval)
|
||||
app.MapExceptionApprovalEndpoints();
|
||||
|
||||
// Governance endpoints (Sprint: SPRINT_20251229_021a_FE_policy_governance_controls, Task: GOV-018)
|
||||
app.MapGovernanceEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)
|
||||
|
||||
@@ -56,12 +56,12 @@ public sealed class ScoringApiContractTests : IAsyncLifetime
|
||||
_pactBuilder = pact.WithHttpInteractions();
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
public Task DisposeAsync()
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
// Pact files are generated when the builder disposes
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
#region Scoring Input Contract Tests
|
||||
@@ -384,8 +384,8 @@ public sealed class ProfileSpecificContractTests : IAsyncLifetime
|
||||
_pactBuilder = pact.WithHttpInteractions();
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact(DisplayName = "Consumer expects Simple profile to return Simple in scoringProfile")]
|
||||
public async Task Consumer_Expects_SimpleProfile_InResponse()
|
||||
@@ -427,3 +427,6 @@ public sealed class ProfileSpecificContractTests : IAsyncLifetime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
</ItemGroup>
|
||||
@@ -28,4 +28,6 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using GatewayProgram = StellaOps.Policy.Gateway.Program;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for governance endpoints (GOV-018).
|
||||
/// </summary>
|
||||
public sealed class GovernanceEndpointsTests : IClassFixture<WebApplicationFactory<GatewayProgram>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly WebApplicationFactory<GatewayProgram> _factory;
|
||||
|
||||
public GovernanceEndpointsTests(WebApplicationFactory<GatewayProgram> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.UseSetting("Environment", "Testing");
|
||||
});
|
||||
|
||||
_client = _factory.CreateClient();
|
||||
_client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant");
|
||||
}
|
||||
|
||||
#region Sealed Mode Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSealedModeStatus_ReturnsOk()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/governance/sealed-mode/status", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.TryGetProperty("isSealed", out _));
|
||||
Assert.True(result.TryGetProperty("tenantId", out _));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSealedModeStatus_ReturnsBadRequest_WhenTenantMissing()
|
||||
{
|
||||
// Arrange
|
||||
var clientWithoutTenant = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await clientWithoutTenant.GetAsync("/api/v1/governance/sealed-mode/status", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ToggleSealedMode_ReturnsOk()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { enable = true, reason = "Test seal" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/sealed-mode/toggle", request, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.GetProperty("isSealed").GetBoolean());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSealedModeOverrides_ReturnsOk()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/governance/sealed-mode/overrides", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.TryGetProperty("overrides", out _));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateSealedModeOverride_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
overrideType = "PolicyUpdate",
|
||||
reason = "Emergency hotfix",
|
||||
durationMinutes = 60,
|
||||
actor = "admin@example.com"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/sealed-mode/overrides", request, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.TryGetProperty("overrideId", out _));
|
||||
Assert.Equal("PolicyUpdate", result.GetProperty("overrideType").GetString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RevokeSealedModeOverride_ReturnsNotFound_WhenOverrideNotExists()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/sealed-mode/overrides/nonexistent/revoke", new { }, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Risk Profile Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ListRiskProfiles_ReturnsOk()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/governance/risk-profiles", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.TryGetProperty("profiles", out _));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateRiskProfile_ReturnsCreated()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = $"test-profile-{Guid.NewGuid():N}";
|
||||
var request = new
|
||||
{
|
||||
profileId = profileId,
|
||||
name = "Test Profile",
|
||||
description = "Test profile for unit tests",
|
||||
maxCriticalFindings = 0,
|
||||
maxHighFindings = 5,
|
||||
maxRiskScore = 7500
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles", request, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(profileId, result.GetProperty("profileId").GetString());
|
||||
Assert.Equal("Test Profile", result.GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetRiskProfile_ReturnsNotFound_WhenProfileNotExists()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/governance/risk-profiles/nonexistent-profile", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGetRiskProfile_RoundTrip()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = $"roundtrip-{Guid.NewGuid():N}";
|
||||
var createRequest = new
|
||||
{
|
||||
profileId = profileId,
|
||||
name = "Roundtrip Profile",
|
||||
description = "Profile for roundtrip test",
|
||||
maxCriticalFindings = 1,
|
||||
maxHighFindings = 10,
|
||||
maxRiskScore = 5000
|
||||
};
|
||||
|
||||
// Act - Create
|
||||
var createResponse = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles", createRequest, cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
||||
|
||||
// Act - Get
|
||||
var getResponse = await _client.GetAsync($"/api/v1/governance/risk-profiles/{profileId}", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
||||
var result = await getResponse.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(profileId, result.GetProperty("profileId").GetString());
|
||||
Assert.Equal("Roundtrip Profile", result.GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateRiskProfile_ReturnsNotFound_WhenProfileNotExists()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
name = "Updated Profile",
|
||||
description = "Updated description",
|
||||
maxCriticalFindings = 2,
|
||||
maxHighFindings = 20,
|
||||
maxRiskScore = 10000
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsJsonAsync("/api/v1/governance/risk-profiles/nonexistent", request, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteRiskProfile_ReturnsNotFound_WhenProfileNotExists()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.DeleteAsync("/api/v1/governance/risk-profiles/nonexistent", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateUpdateDeleteRiskProfile_FullLifecycle()
|
||||
{
|
||||
// Arrange
|
||||
var profileId = $"lifecycle-{Guid.NewGuid():N}";
|
||||
|
||||
// Act - Create
|
||||
var createRequest = new
|
||||
{
|
||||
profileId = profileId,
|
||||
name = "Lifecycle Profile",
|
||||
description = "Profile for lifecycle test",
|
||||
maxCriticalFindings = 0,
|
||||
maxHighFindings = 5,
|
||||
maxRiskScore = 7500
|
||||
};
|
||||
var createResponse = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles", createRequest, cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
||||
|
||||
// Act - Update
|
||||
var updateRequest = new
|
||||
{
|
||||
name = "Updated Lifecycle Profile",
|
||||
description = "Updated description",
|
||||
maxCriticalFindings = 1,
|
||||
maxHighFindings = 10,
|
||||
maxRiskScore = 10000
|
||||
};
|
||||
var updateResponse = await _client.PutAsJsonAsync($"/api/v1/governance/risk-profiles/{profileId}", updateRequest, cancellationToken: CancellationToken.None);
|
||||
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
|
||||
|
||||
// Verify update
|
||||
var getResponse = await _client.GetAsync($"/api/v1/governance/risk-profiles/{profileId}", CancellationToken.None);
|
||||
var profile = await getResponse.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.Equal("Updated Lifecycle Profile", profile.GetProperty("name").GetString());
|
||||
|
||||
// Act - Delete
|
||||
var deleteResponse = await _client.DeleteAsync($"/api/v1/governance/risk-profiles/{profileId}", CancellationToken.None);
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
||||
|
||||
// Verify deletion
|
||||
var getAfterDeleteResponse = await _client.GetAsync($"/api/v1/governance/risk-profiles/{profileId}", CancellationToken.None);
|
||||
Assert.Equal(HttpStatusCode.NotFound, getAfterDeleteResponse.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ActivateRiskProfile_ReturnsNotFound_WhenProfileNotExists()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles/nonexistent/activate", new { }, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeprecateRiskProfile_ReturnsNotFound_WhenProfileNotExists()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles/nonexistent/deprecate", new { }, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateRiskProfile_ReturnsValidationResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
name = "Valid Profile",
|
||||
description = "Test description",
|
||||
maxCriticalFindings = 0,
|
||||
maxHighFindings = 5,
|
||||
maxRiskScore = 7500
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles/validate", request, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.TryGetProperty("isValid", out _));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateRiskProfile_ReturnsInvalid_WithNegativeValues()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
name = "",
|
||||
maxCriticalFindings = -1,
|
||||
maxHighFindings = -1,
|
||||
maxRiskScore = -1
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/governance/risk-profiles/validate", request, cancellationToken: CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.False(result.GetProperty("isValid").GetBoolean());
|
||||
Assert.True(result.TryGetProperty("errors", out var errors));
|
||||
Assert.True(errors.GetArrayLength() > 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Audit Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAuditEvents_ReturnsOk()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/governance/audit/events", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: CancellationToken.None);
|
||||
Assert.True(result.TryGetProperty("events", out _));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAuditEvent_ReturnsNotFound_WhenEventNotExists()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/governance/audit/events/nonexistent-event", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAuditEvents_ReturnsBadRequest_WhenTenantMissing()
|
||||
{
|
||||
// Arrange
|
||||
var clientWithoutTenant = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await clientWithoutTenant.GetAsync("/api/v1/governance/audit/events", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -53,14 +53,14 @@ public sealed class PolicyGatewayIntegrationTests : IAsyncLifetime
|
||||
ActivitySource.AddActivityListener(_activityListener);
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
_factory = new PolicyGatewayTestFactory();
|
||||
_client = _factory.CreateClient();
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_activityListener.Dispose();
|
||||
_client.Dispose();
|
||||
@@ -483,3 +483,6 @@ public sealed record CreateExceptionRequest
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class EvaluationRunRepositoryTests : IAsyncLifetime
|
||||
_repository = new EvaluationRunRepository(dataSource, NullLogger<EvaluationRunRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed class EvaluationRunRepositoryTests : IAsyncLifetime
|
||||
};
|
||||
await _packVersionRepository.CreateAsync(packVersion);
|
||||
}
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -300,3 +300,6 @@ public sealed class EvaluationRunRepositoryTests : IAsyncLifetime
|
||||
Status = EvaluationStatus.Pending
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ public sealed class ExceptionObjectRepositoryTests : IAsyncLifetime
|
||||
_repository = new PostgresExceptionObjectRepository(dataSource, NullLogger<PostgresExceptionObjectRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -485,3 +485,6 @@ public sealed class ExceptionObjectRepositoryTests : IAsyncLifetime
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ public sealed class ExceptionRepositoryTests : IAsyncLifetime
|
||||
_repository = new ExceptionRepository(dataSource, NullLogger<ExceptionRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -289,3 +289,6 @@ public sealed class ExceptionRepositoryTests : IAsyncLifetime
|
||||
Status = ExceptionStatus.Active
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ public sealed class PackRepositoryTests : IAsyncLifetime
|
||||
_packVersionRepository = new PackVersionRepository(dataSource, NullLogger<PackVersionRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -283,3 +283,6 @@ public sealed class PackRepositoryTests : IAsyncLifetime
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ public sealed class PackVersioningWorkflowTests : IAsyncLifetime
|
||||
_ruleRepository = new RuleRepository(dataSource, NullLogger<RuleRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -290,3 +290,6 @@ public sealed class PackVersioningWorkflowTests : IAsyncLifetime
|
||||
updated.IsBuiltin.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ public sealed class PolicyAuditRepositoryTests : IAsyncLifetime
|
||||
_repository = new PolicyAuditRepository(dataSource, NullLogger<PolicyAuditRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -200,3 +200,6 @@ public sealed class PolicyAuditRepositoryTests : IAsyncLifetime
|
||||
ResourceId = Guid.NewGuid().ToString()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class PolicyMigrationTests : IAsyncLifetime
|
||||
{
|
||||
private PostgreSqlContainer _container = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_container = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
@@ -41,7 +41,7 @@ public sealed class PolicyMigrationTests : IAsyncLifetime
|
||||
await _container.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _container.DisposeAsync();
|
||||
}
|
||||
@@ -320,3 +320,6 @@ public sealed class PolicyMigrationTests : IAsyncLifetime
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public sealed class PolicyTestKitPostgresFixture : IAsyncLifetime
|
||||
public TestKitPostgresFixture Fixture => _fixture;
|
||||
public string ConnectionString => _fixture.ConnectionString;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_fixture = new TestKitPostgresFixture();
|
||||
_fixture.IsolationMode = TestKitPostgresIsolationMode.Truncation;
|
||||
@@ -59,7 +59,7 @@ public sealed class PolicyTestKitPostgresFixture : IAsyncLifetime
|
||||
await _fixture.ApplyMigrationsFromAssemblyAsync(MigrationAssembly, "public");
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => _fixture.DisposeAsync();
|
||||
public ValueTask DisposeAsync() => _fixture.DisposeAsync();
|
||||
|
||||
public Task TruncateAllTablesAsync() => _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
@@ -72,3 +72,6 @@ public sealed class PolicyTestKitPostgresCollection : ICollectionFixture<PolicyT
|
||||
{
|
||||
public const string Name = "PolicyTestKitPostgres";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
_auditRepository = new PolicyAuditRepository(_dataSource, NullLogger<PolicyAuditRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllPacks_MultipleQueries_ReturnsDeterministicOrder()
|
||||
@@ -410,3 +410,6 @@ public sealed class PolicyQueryDeterminismTests : IAsyncLifetime
|
||||
return await _auditRepository.CreateAsync(audit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class PolicyVersioningImmutabilityTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class PolicyVersioningImmutabilityTests : IAsyncLifetime
|
||||
_ruleRepository = new RuleRepository(_dataSource, NullLogger<RuleRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task PublishedVersion_CannotBeDeleted()
|
||||
@@ -302,3 +302,6 @@ public sealed class PolicyVersioningImmutabilityTests : IAsyncLifetime
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class PostgresExceptionApplicationRepositoryTests : IAsyncLifetime
|
||||
_repository = new PostgresExceptionApplicationRepository(_dataSource);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class PostgresExceptionApplicationRepositoryTests : IAsyncLifetime
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
@@ -175,3 +175,6 @@ public sealed class PostgresExceptionApplicationRepositoryTests : IAsyncLifetime
|
||||
string eff = "suppress") =>
|
||||
ExceptionApplication.Create(_tenantId, excId, findId, "affected", "not_affected", "test", eff, vulnId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ public sealed class PostgresExceptionObjectRepositoryTests : IAsyncLifetime
|
||||
_repository = new PostgresExceptionObjectRepository(dataSource, NullLogger<PostgresExceptionObjectRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
#region Create Tests
|
||||
|
||||
@@ -569,3 +569,6 @@ public sealed class PostgresExceptionObjectRepositoryTests : IAsyncLifetime
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ public sealed class PostgresReceiptRepositoryTests : IAsyncLifetime
|
||||
_repository = new PostgresReceiptRepository(dataSource, NullLogger<PostgresReceiptRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -110,3 +110,6 @@ public sealed class PostgresReceiptRepositoryTests : IAsyncLifetime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ public sealed class RecheckEvidenceMigrationTests : IAsyncLifetime
|
||||
_dataSource = new PolicyDataSource(Options.Create(options), NullLogger<PolicyDataSource>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -48,3 +48,6 @@ public sealed class RecheckEvidenceMigrationTests : IAsyncLifetime
|
||||
result.Should().NotBeNull($"{tableName} should exist after migrations");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ public sealed class RiskProfileRepositoryTests : IAsyncLifetime
|
||||
_repository = new RiskProfileRepository(dataSource, NullLogger<RiskProfileRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -360,3 +360,6 @@ public sealed class RiskProfileRepositoryTests : IAsyncLifetime
|
||||
ScoringWeights = scoringWeights ?? "{}"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ public sealed class RiskProfileVersionHistoryTests : IAsyncLifetime
|
||||
_repository = new RiskProfileRepository(dataSource, NullLogger<RiskProfileRepository>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -485,3 +485,6 @@ public sealed class RiskProfileVersionHistoryTests : IAsyncLifetime
|
||||
afterUpdate.UpdatedAt.Should().BeOnOrAfter(createTime); // UpdatedAt should progress
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class RuleRepositoryTests : IAsyncLifetime
|
||||
_repository = new RuleRepository(dataSource, NullLogger<RuleRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed class RuleRepositoryTests : IAsyncLifetime
|
||||
|
||||
await _packVersionRepository.CreateAsync(packVersion);
|
||||
}
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -279,3 +279,6 @@ public sealed class RuleRepositoryTests : IAsyncLifetime
|
||||
ContentHash = Guid.NewGuid().ToString()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
_dataSource = new PolicyDataSource(Options.Create(options), NullLogger<PolicyDataSource>.Instance);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public async Task DisposeAsync() => await _dataSource.DisposeAsync();
|
||||
public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync());
|
||||
public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync();
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -123,3 +123,6 @@ public sealed class UnknownsRepositoryTests : IAsyncLifetime
|
||||
UpdatedAt = timestamp
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -21,4 +21,6 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -25,4 +25,6 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user