271 lines
8.9 KiB
C#
271 lines
8.9 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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, TestContext.Current.CancellationToken);
|
|
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
// 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, TestContext.Current.CancellationToken);
|
|
|
|
// 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", "/healthz")]
|
|
[InlineData("PUT", "/healthz")]
|
|
[InlineData("PATCH", "/healthz")]
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
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", TestContext.Current.CancellationToken);
|
|
|
|
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")]
|
|
public async Task Get_WithInvalidGuid_Returns400Or404(string endpoint)
|
|
{
|
|
using var client = _factory.CreateClient();
|
|
|
|
var response = await client.GetAsync(endpoint, TestContext.Current.CancellationToken);
|
|
|
|
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, TestContext.Current.CancellationToken);
|
|
|
|
// 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("/healthz", TestContext.Current.CancellationToken));
|
|
|
|
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
|
|
}
|