// ----------------------------------------------------------------------------- // 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; /// /// Negative tests for Scanner.WebService. /// Verifies proper error handling for invalid requests. /// [Trait("Category", TestCategories.Integration)] [Collection("ScannerWebService")] public sealed class ScannerNegativeTests : IClassFixture { private readonly ScannerApplicationFactory _factory; public ScannerNegativeTests(ScannerApplicationFactory factory) { _factory = factory; } #region Content-Type Tests (415 Unsupported Media Type) /// /// Verifies that POST with wrong content type returns 415. /// [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"); } /// /// Verifies that missing content type returns appropriate error. /// [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) /// /// Verifies that oversized payload returns 413. /// [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) /// /// Verifies that wrong HTTP method returns 405. /// [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) /// /// Verifies that malformed JSON returns 400. /// [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); } /// /// Verifies that empty body returns 400. /// [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); } /// /// Verifies that missing required fields returns 400. /// [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) /// /// Verifies that non-existent resource returns 404. /// [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); } /// /// Verifies that non-existent endpoint returns 404. /// [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 /// /// Verifies that invalid GUID format returns 400. /// [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); } /// /// Verifies that SQL injection attempts are rejected. /// [Theory] [InlineData("/api/v1/scans?filter=1'; DROP TABLE scans;--")] [InlineData("/api/v1/scans?search=")] 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) /// /// Verifies that rapid requests are rate limited. /// [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 }