Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (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
Policy Simulation / policy-simulate (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (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
Policy Simulation / policy-simulate (push) Has been cancelled
- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management. - Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management. - Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support. - Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
@@ -325,6 +325,47 @@ public sealed record VexStatusChange
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to verify an evidence bundle.
|
||||
/// Sprint: SPRINT_3602_0001_0001 - Task 10
|
||||
/// </summary>
|
||||
public sealed record BundleVerificationRequest
|
||||
{
|
||||
[JsonPropertyName("bundle_hash")]
|
||||
public required string BundleHash { get; init; }
|
||||
|
||||
[JsonPropertyName("signature")]
|
||||
public string? Signature { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for bundle verification.
|
||||
/// Sprint: SPRINT_3602_0001_0001 - Task 10
|
||||
/// </summary>
|
||||
public sealed record BundleVerificationResponse
|
||||
{
|
||||
[JsonPropertyName("alert_id")]
|
||||
public required string AlertId { get; init; }
|
||||
|
||||
[JsonPropertyName("is_valid")]
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
[JsonPropertyName("verified_at")]
|
||||
public required DateTimeOffset VerifiedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("signature_valid")]
|
||||
public bool SignatureValid { get; init; }
|
||||
|
||||
[JsonPropertyName("hash_valid")]
|
||||
public bool HashValid { get; init; }
|
||||
|
||||
[JsonPropertyName("chain_valid")]
|
||||
public bool ChainValid { get; init; }
|
||||
|
||||
[JsonPropertyName("errors")]
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle verification result.
|
||||
/// </summary>
|
||||
|
||||
@@ -1677,6 +1677,77 @@ app.MapGet("/v1/alerts/{alertId}/audit", async Task<Results<JsonHttpResult<Audit
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Sprint: SPRINT_3602_0001_0001 - Task 9: Bundle download endpoint
|
||||
app.MapGet("/v1/alerts/{alertId}/bundle", async Task<Results<FileStreamHttpResult, NotFound, ProblemHttpResult>> (
|
||||
string alertId,
|
||||
[FromServices] IAlertService alertService,
|
||||
[FromServices] IEvidenceBundleService bundleService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var alert = await alertService.GetAlertAsync(alertId, cancellationToken).ConfigureAwait(false);
|
||||
if (alert is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var bundle = await bundleService.CreateBundleAsync(alertId, cancellationToken).ConfigureAwait(false);
|
||||
if (bundle is null)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
detail: "Failed to create evidence bundle",
|
||||
statusCode: StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
|
||||
return TypedResults.File(
|
||||
bundle.Content,
|
||||
contentType: "application/gzip",
|
||||
fileDownloadName: $"evidence-{alertId}.tar.gz");
|
||||
})
|
||||
.WithName("DownloadAlertBundle")
|
||||
.RequireAuthorization(AlertReadPolicy)
|
||||
.Produces<FileStreamHttpResult>(StatusCodes.Status200OK, "application/gzip")
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Sprint: SPRINT_3602_0001_0001 - Task 10: Bundle verify endpoint
|
||||
app.MapPost("/v1/alerts/{alertId}/bundle/verify", async Task<Results<Ok<BundleVerificationResponse>, NotFound, ProblemHttpResult>> (
|
||||
string alertId,
|
||||
[FromBody] BundleVerificationRequest request,
|
||||
[FromServices] IAlertService alertService,
|
||||
[FromServices] IEvidenceBundleService bundleService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var alert = await alertService.GetAlertAsync(alertId, cancellationToken).ConfigureAwait(false);
|
||||
if (alert is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var result = await bundleService.VerifyBundleAsync(
|
||||
alertId,
|
||||
request.BundleHash,
|
||||
request.Signature,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new BundleVerificationResponse
|
||||
{
|
||||
AlertId = alertId,
|
||||
IsValid = result.IsValid,
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
SignatureValid = result.SignatureValid,
|
||||
HashValid = result.HashValid,
|
||||
ChainValid = result.ChainValid,
|
||||
Errors = result.Errors
|
||||
};
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
})
|
||||
.WithName("VerifyAlertBundle")
|
||||
.RequireAuthorization(AlertReadPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapPost("/v1/vex-consensus/issuers", async Task<Results<Created<VexIssuerDetailResponse>, ProblemHttpResult>> (
|
||||
RegisterVexIssuerRequest request,
|
||||
VexConsensusService consensusService,
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// =============================================================================
|
||||
// EvidenceDecisionApiIntegrationTests.cs
|
||||
// Sprint: SPRINT_3602_0001_0001
|
||||
// Task: 12 - API integration tests
|
||||
// =============================================================================
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for Evidence and Decision API endpoints.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "3602")]
|
||||
public sealed class EvidenceDecisionApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public EvidenceDecisionApiIntegrationTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GET /v1/alerts returns paginated list")]
|
||||
public async Task GetAlerts_ReturnsPaginatedList()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/v1/alerts?limit=10");
|
||||
|
||||
// Assert
|
||||
// Note: In actual test, would need auth token
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.OK,
|
||||
HttpStatusCode.Unauthorized); // Depends on test auth setup
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GET /v1/alerts with filters applies correctly")]
|
||||
public async Task GetAlerts_WithFilters_AppliesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var filters = "?band=critical&status=open&limit=5";
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/v1/alerts{filters}");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.OK,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GET /v1/alerts/{id} returns 404 for non-existent alert")]
|
||||
public async Task GetAlert_NonExistent_Returns404()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/v1/alerts/non-existent-id");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /v1/alerts/{id}/decisions requires decision and rationale")]
|
||||
public async Task PostDecision_RequiresFields()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
decision = "accept_risk",
|
||||
rationale = "Test rationale for decision"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/v1/alerts/test-id/decisions", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.Created,
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized,
|
||||
HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /v1/alerts/{id}/decisions rejects empty rationale")]
|
||||
public async Task PostDecision_EmptyRationale_Rejected()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
decision = "accept_risk",
|
||||
rationale = ""
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/v1/alerts/test-id/decisions", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GET /v1/alerts/{id}/audit returns timeline")]
|
||||
public async Task GetAudit_ReturnsTimeline()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/v1/alerts/test-id/audit");
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.OK,
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GET /v1/alerts/{id}/bundle returns gzip content-type")]
|
||||
public async Task GetBundle_ReturnsGzip()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/v1/alerts/test-id/bundle");
|
||||
|
||||
// Assert
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
response.Content.Headers.ContentType?.MediaType.Should().Be("application/gzip");
|
||||
}
|
||||
else
|
||||
{
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "POST /v1/alerts/{id}/bundle/verify validates hash")]
|
||||
public async Task VerifyBundle_ValidatesHash()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
bundle_hash = "sha256:abc123",
|
||||
signature = "test-signature"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/v1/alerts/test-id/bundle/verify", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.OK,
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "API returns proper error format for invalid requests")]
|
||||
public async Task InvalidRequest_ReturnsProblemDetails()
|
||||
{
|
||||
// Arrange
|
||||
var invalidJson = "not-json";
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsync(
|
||||
"/v1/alerts/test-id/decisions",
|
||||
new StringContent(invalidJson, System.Text.Encoding.UTF8, "application/json"));
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.UnsupportedMediaType,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// =============================================================================
|
||||
// OpenApiSchemaTests.cs
|
||||
// Sprint: SPRINT_3602_0001_0001
|
||||
// Task: 13 - OpenAPI schema validation tests
|
||||
// =============================================================================
|
||||
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Findings.Ledger.Tests.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Tests to validate API response contracts match OpenAPI specification.
|
||||
/// </summary>
|
||||
[Trait("Category", "Schema")]
|
||||
[Trait("Sprint", "3602")]
|
||||
public sealed class OpenApiSchemaTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
[Fact(DisplayName = "AlertSummary serializes with correct property names")]
|
||||
public void AlertSummary_SerializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var alert = new AlertSummary
|
||||
{
|
||||
AlertId = "alert-123",
|
||||
ArtifactId = "sha256:abc",
|
||||
VulnId = "CVE-2024-1234",
|
||||
ComponentPurl = "pkg:npm/lodash@4.17.21",
|
||||
Severity = "HIGH",
|
||||
Band = "critical",
|
||||
Status = "open",
|
||||
Score = 9.5,
|
||||
CreatedAt = DateTimeOffset.Parse("2024-12-15T10:00:00Z"),
|
||||
UpdatedAt = DateTimeOffset.Parse("2024-12-16T10:00:00Z"),
|
||||
DecisionCount = 2
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(alert, JsonOptions);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert - verify snake_case property names per OpenAPI spec
|
||||
root.TryGetProperty("alert_id", out _).Should().BeTrue();
|
||||
root.TryGetProperty("artifact_id", out _).Should().BeTrue();
|
||||
root.TryGetProperty("vuln_id", out _).Should().BeTrue();
|
||||
root.TryGetProperty("component_purl", out _).Should().BeTrue();
|
||||
root.TryGetProperty("severity", out _).Should().BeTrue();
|
||||
root.TryGetProperty("band", out _).Should().BeTrue();
|
||||
root.TryGetProperty("status", out _).Should().BeTrue();
|
||||
root.TryGetProperty("score", out _).Should().BeTrue();
|
||||
root.TryGetProperty("created_at", out _).Should().BeTrue();
|
||||
root.TryGetProperty("decision_count", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AlertListResponse includes required fields")]
|
||||
public void AlertListResponse_IncludesRequiredFields()
|
||||
{
|
||||
// Arrange
|
||||
var response = new AlertListResponse(
|
||||
Items: new List<AlertSummary>(),
|
||||
TotalCount: 0,
|
||||
NextPageToken: null);
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(response, JsonOptions);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert - items and total_count are required per OpenAPI spec
|
||||
root.TryGetProperty("items", out var items).Should().BeTrue();
|
||||
items.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
|
||||
root.TryGetProperty("total_count", out var count).Should().BeTrue();
|
||||
count.ValueKind.Should().Be(JsonValueKind.Number);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DecisionRequest validates required fields")]
|
||||
public void DecisionRequest_RequiresFields()
|
||||
{
|
||||
// Arrange
|
||||
var request = new DecisionRequest
|
||||
{
|
||||
Decision = "accept_risk",
|
||||
Rationale = "Test rationale",
|
||||
JustificationCode = null,
|
||||
Metadata = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert - decision and rationale are required per OpenAPI spec
|
||||
root.TryGetProperty("decision", out var decision).Should().BeTrue();
|
||||
decision.GetString().Should().NotBeNullOrEmpty();
|
||||
|
||||
root.TryGetProperty("rationale", out var rationale).Should().BeTrue();
|
||||
rationale.GetString().Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "BundleVerificationResponse includes all fields")]
|
||||
public void BundleVerificationResponse_IncludesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
var response = new BundleVerificationResponse
|
||||
{
|
||||
AlertId = "alert-123",
|
||||
IsValid = true,
|
||||
VerifiedAt = DateTimeOffset.UtcNow,
|
||||
SignatureValid = true,
|
||||
HashValid = true,
|
||||
ChainValid = true,
|
||||
Errors = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(response, JsonOptions);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert - verify required fields per OpenAPI spec
|
||||
root.TryGetProperty("alert_id", out _).Should().BeTrue();
|
||||
root.TryGetProperty("is_valid", out _).Should().BeTrue();
|
||||
root.TryGetProperty("verified_at", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AuditTimelineResponse serializes correctly")]
|
||||
public void AuditTimelineResponse_SerializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var response = new AuditTimelineResponse
|
||||
{
|
||||
AlertId = "alert-123",
|
||||
Events = new List<AuditEventResponse>(),
|
||||
TotalCount = 0
|
||||
};
|
||||
|
||||
// Act
|
||||
var json = JsonSerializer.Serialize(response, JsonOptions);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Assert
|
||||
root.TryGetProperty("alert_id", out _).Should().BeTrue();
|
||||
root.TryGetProperty("events", out var events).Should().BeTrue();
|
||||
events.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
root.TryGetProperty("total_count", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Decision enum values match OpenAPI spec")]
|
||||
public void DecisionEnumValues_MatchSpec()
|
||||
{
|
||||
// Arrange - valid decision values per OpenAPI spec
|
||||
var validDecisions = new[] { "accept_risk", "mitigate", "suppress", "escalate" };
|
||||
|
||||
// Assert - all values should be accepted
|
||||
foreach (var decision in validDecisions)
|
||||
{
|
||||
var request = new DecisionRequest
|
||||
{
|
||||
Decision = decision,
|
||||
Rationale = "Test rationale"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||||
json.Should().Contain(decision);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Band enum values match OpenAPI spec")]
|
||||
public void BandEnumValues_MatchSpec()
|
||||
{
|
||||
// Arrange - valid band values per OpenAPI spec
|
||||
var validBands = new[] { "critical", "high", "medium", "low", "info" };
|
||||
|
||||
// Assert - all values should be representable
|
||||
foreach (var band in validBands)
|
||||
{
|
||||
var alert = new AlertSummary
|
||||
{
|
||||
AlertId = "test",
|
||||
ArtifactId = "test",
|
||||
VulnId = "test",
|
||||
Severity = "test",
|
||||
Band = band,
|
||||
Status = "open",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(alert, JsonOptions);
|
||||
json.Should().Contain($"\"{band}\"");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Status enum values match OpenAPI spec")]
|
||||
public void StatusEnumValues_MatchSpec()
|
||||
{
|
||||
// Arrange - valid status values per OpenAPI spec
|
||||
var validStatuses = new[] { "open", "acknowledged", "resolved", "suppressed" };
|
||||
|
||||
// Assert - all values should be representable
|
||||
foreach (var status in validStatuses)
|
||||
{
|
||||
var alert = new AlertSummary
|
||||
{
|
||||
AlertId = "test",
|
||||
ArtifactId = "test",
|
||||
VulnId = "test",
|
||||
Severity = "test",
|
||||
Band = "critical",
|
||||
Status = status,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(alert, JsonOptions);
|
||||
json.Should().Contain($"\"{status}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user