Add determinism tests for verdict artifact generation and update SHA256 sums script
- Implemented comprehensive tests for verdict artifact generation to ensure deterministic outputs across various scenarios, including identical inputs, parallel execution, and change ordering. - Created helper methods for generating sample verdict inputs and computing canonical hashes. - Added tests to validate the stability of canonical hashes, proof spine ordering, and summary statistics. - Introduced a new PowerShell script to update SHA256 sums for files, ensuring accurate hash generation and file integrity checks.
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ScannerNegativeTests.cs
|
||||
// Sprint: SPRINT_5100_0007_0006_webservice_contract
|
||||
// Task: WEBSVC-5100-009
|
||||
// Description: Negative tests for Scanner.WebService (error handling validation)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests.Negative;
|
||||
|
||||
/// <summary>
|
||||
/// Negative tests for Scanner.WebService.
|
||||
/// Verifies proper error handling for invalid requests.
|
||||
/// </summary>
|
||||
[Trait("Category", TestCategories.Integration)]
|
||||
[Collection("ScannerWebService")]
|
||||
public sealed class ScannerNegativeTests : IClassFixture<ScannerApplicationFactory>
|
||||
{
|
||||
private readonly ScannerApplicationFactory _factory;
|
||||
|
||||
public ScannerNegativeTests(ScannerApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
#region Content-Type Tests (415 Unsupported Media Type)
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that POST with wrong content type returns 415.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("text/plain")]
|
||||
[InlineData("text/html")]
|
||||
[InlineData("application/xml")]
|
||||
public async Task Post_WithWrongContentType_Returns415(string contentType)
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var content = new StringContent("{\"test\": true}", Encoding.UTF8, contentType);
|
||||
var response = await client.PostAsync("/api/v1/scans", content);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType,
|
||||
$"POST with content-type '{contentType}' should return 415");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that missing content type returns appropriate error.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Post_WithMissingContentType_ReturnsError()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var content = new StringContent("{\"test\": true}", Encoding.UTF8);
|
||||
content.Headers.ContentType = null;
|
||||
|
||||
var response = await client.PostAsync("/api/v1/scans", content);
|
||||
|
||||
// Should be either 415 or 400 depending on implementation
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.UnsupportedMediaType,
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Payload Size Tests (413 Payload Too Large)
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that oversized payload returns 413.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Post_WithOversizedPayload_Returns413()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
// Create a 50MB payload (assuming limit is lower)
|
||||
var largeContent = new string('x', 50 * 1024 * 1024);
|
||||
var content = new StringContent($"{{\"data\": \"{largeContent}\"}}", Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.PostAsync("/api/v1/scans", content);
|
||||
|
||||
// Should be 413 or the request might timeout/fail
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.RequestEntityTooLarge,
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Method Mismatch Tests (405 Method Not Allowed)
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that wrong HTTP method returns 405.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("DELETE", "/api/v1/health")]
|
||||
[InlineData("PUT", "/api/v1/health")]
|
||||
[InlineData("PATCH", "/api/v1/health")]
|
||||
public async Task WrongMethod_Returns405(string method, string endpoint)
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var request = new HttpRequestMessage(new HttpMethod(method), endpoint);
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed,
|
||||
$"{method} {endpoint} should return 405");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Malformed Request Tests (400 Bad Request)
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that malformed JSON returns 400.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Post_WithMalformedJson_Returns400()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var content = new StringContent("{ invalid json }", Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync("/api/v1/scans", content);
|
||||
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that empty body returns 400.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Post_WithEmptyBody_Returns400()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync("/api/v1/scans", content);
|
||||
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that missing required fields returns 400.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Post_WithMissingRequiredFields_Returns400()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var content = new StringContent("{}", Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync("/api/v1/scans", content);
|
||||
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.Unauthorized,
|
||||
HttpStatusCode.UnprocessableEntity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Not Found Tests (404)
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that non-existent resource returns 404.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("/api/v1/scans/00000000-0000-0000-0000-000000000000")]
|
||||
[InlineData("/api/v1/sbom/00000000-0000-0000-0000-000000000000")]
|
||||
public async Task Get_NonExistentResource_Returns404(string endpoint)
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync(endpoint);
|
||||
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that non-existent endpoint returns 404.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Get_NonExistentEndpoint_Returns404()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/api/v1/nonexistent");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Invalid Parameter Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that invalid GUID format returns 400.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("/api/v1/scans/not-a-guid")]
|
||||
[InlineData("/api/v1/scans/12345")]
|
||||
[InlineData("/api/v1/scans/")]
|
||||
public async Task Get_WithInvalidGuid_Returns400Or404(string endpoint)
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync(endpoint);
|
||||
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.BadRequest,
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that SQL injection attempts are rejected.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("/api/v1/scans?filter=1'; DROP TABLE scans;--")]
|
||||
[InlineData("/api/v1/scans?search=<script>alert('xss')</script>")]
|
||||
public async Task Get_WithInjectionAttempt_ReturnsSafeResponse(string endpoint)
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync(endpoint);
|
||||
|
||||
// Should not cause server error (500)
|
||||
response.StatusCode.Should().NotBe(HttpStatusCode.InternalServerError,
|
||||
"Injection attempts should not cause server errors");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rate Limiting Tests (429)
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that rapid requests are rate limited.
|
||||
/// </summary>
|
||||
[Fact(Skip = "Rate limiting may not be enabled in test environment")]
|
||||
public async Task RapidRequests_AreRateLimited()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
|
||||
var tasks = Enumerable.Range(0, 100)
|
||||
.Select(_ => client.GetAsync("/api/v1/health"));
|
||||
|
||||
var responses = await Task.WhenAll(tasks);
|
||||
|
||||
var tooManyRequests = responses.Count(r =>
|
||||
r.StatusCode == HttpStatusCode.TooManyRequests);
|
||||
|
||||
// Some requests should be rate limited
|
||||
tooManyRequests.Should().BeGreaterThan(0,
|
||||
"Rate limiting should kick in for rapid requests");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user