test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
using System.Security.Claims;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using NSubstitute;
|
|
|
|
|
using ProposalSystem.Api.Middleware;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
namespace ProposalSystem.Tests.Middleware;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// QA-C4: InternalApiKeyMiddleware tests.
|
|
|
|
|
/// Validates that internal API key authentication works correctly:
|
|
|
|
|
/// - Valid key on any path sets system claims and calls next (current behavior)
|
|
|
|
|
/// - Invalid key logs warning but still calls next (passes through to JWT)
|
|
|
|
|
/// - Missing key header passes through to next middleware (JWT auth)
|
|
|
|
|
/// - Empty key in config disables the middleware entirely
|
|
|
|
|
///
|
|
|
|
|
/// Note: API-C1 audit finding identified that this middleware applies globally
|
|
|
|
|
/// and bypasses JWT on ANY route when a valid key is provided. These tests
|
|
|
|
|
/// document the current behavior; the fix should scope keys to /internal/ paths.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class InternalApiKeyMiddlewareTests
|
|
|
|
|
{
|
|
|
|
|
private const string ValidApiKey = "test-internal-api-key-secret-value";
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4: Valid key sets system claims and calls next")]
|
|
|
|
|
public async Task ValidKey_SetsClaimsAndCallsNext()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = ValidApiKey;
|
|
|
|
|
context.Request.Path = "/api/proposals/123";
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
nextCalled().Should().BeTrue("next middleware should be called");
|
|
|
|
|
context.User.Identity!.IsAuthenticated.Should().BeTrue();
|
|
|
|
|
context.User.Identity!.AuthenticationType.Should().Be("InternalApiKey");
|
|
|
|
|
context.User.FindFirst(ClaimTypes.NameIdentifier)!.Value.Should().Be("system");
|
|
|
|
|
context.User.FindFirst("sub")!.Value.Should().Be("system-lambda-caller");
|
|
|
|
|
context.User.FindFirst(ClaimTypes.Role)!.Value.Should().Be("admins");
|
|
|
|
|
context.User.FindFirst("cognito:groups")!.Value.Should().Be("admins");
|
|
|
|
|
context.User.FindFirst(ClaimTypes.Email)!.Value.Should().Be("system@proposal-system.internal");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 17:46:50 -04:00
|
|
|
[Fact(DisplayName = "QA-C4/API-H1: Invalid key returns 401 immediately")]
|
|
|
|
|
public async Task InvalidKey_Returns401()
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = "wrong-key";
|
|
|
|
|
context.Request.Path = "/api/proposals/123";
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
2026-05-27 17:46:50 -04:00
|
|
|
// Assert — API-H1 fix: invalid key short-circuits with 401
|
|
|
|
|
nextCalled().Should().BeFalse("invalid key should not fall through");
|
|
|
|
|
context.Response.StatusCode.Should().Be(401);
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4: Missing key header passes through to next middleware")]
|
|
|
|
|
public async Task MissingKeyHeader_PassesThrough()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
// No X-Internal-Api-Key header set
|
|
|
|
|
context.Request.Path = "/api/proposals";
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
nextCalled().Should().BeTrue("request should pass through to next middleware");
|
|
|
|
|
context.User.Identity!.IsAuthenticated.Should().BeFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4: Empty key in config disables API key check entirely")]
|
|
|
|
|
public async Task EmptyKeyInConfig_DisablesMiddleware()
|
|
|
|
|
{
|
|
|
|
|
// Arrange - empty config key means middleware is effectively disabled
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware("");
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = "any-key-value";
|
|
|
|
|
context.Request.Path = "/api/proposals/123";
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
nextCalled().Should().BeTrue("next middleware should be called");
|
|
|
|
|
context.User.Identity!.IsAuthenticated.Should().BeFalse(
|
|
|
|
|
"middleware is disabled when config key is empty");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4: Null config key disables API key check")]
|
|
|
|
|
public async Task NullConfigKey_DisablesMiddleware()
|
|
|
|
|
{
|
|
|
|
|
// Arrange - null config key (INTERNAL_API_KEY not set)
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(null);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = "any-key-value";
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
nextCalled().Should().BeTrue();
|
|
|
|
|
context.User.Identity!.IsAuthenticated.Should().BeFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4: Empty header value passes through")]
|
|
|
|
|
public async Task EmptyHeaderValue_PassesThrough()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = "";
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
nextCalled().Should().BeTrue();
|
|
|
|
|
context.User.Identity!.IsAuthenticated.Should().BeFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4: Timing-safe comparison used (key differs by one char)")]
|
2026-05-27 17:46:50 -04:00
|
|
|
public async Task SimilarKey_Returns401()
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
{
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = ValidApiKey + "x";
|
2026-05-27 17:46:50 -04:00
|
|
|
context.Request.Path = "/api/proposals/123";
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
2026-05-27 17:46:50 -04:00
|
|
|
// Assert — API-H1 fix: invalid key short-circuits with 401
|
|
|
|
|
nextCalled().Should().BeFalse("similar but wrong key should not fall through");
|
|
|
|
|
context.Response.StatusCode.Should().Be(401);
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 17:46:50 -04:00
|
|
|
[Fact(DisplayName = "QA-C4/API-C1: Valid key on allowed path authenticates")]
|
|
|
|
|
public async Task ValidKey_AllowedPath_Authenticates()
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
{
|
2026-05-27 17:46:50 -04:00
|
|
|
var allowedPaths = new[] { "/api/proposals", "/api/vendor-proposals/1", "/api/generated-pdfs", "/api/files/upload" };
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
|
2026-05-27 17:46:50 -04:00
|
|
|
foreach (var path in allowedPaths)
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
{
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = ValidApiKey;
|
|
|
|
|
context.Request.Path = path;
|
|
|
|
|
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
context.User.Identity!.IsAuthenticated.Should().BeTrue(
|
2026-05-27 17:46:50 -04:00
|
|
|
$"valid key should authenticate on allowed path {path}");
|
|
|
|
|
nextCalled().Should().BeTrue();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact(DisplayName = "QA-C4/API-C1: Valid key on disallowed path returns 403")]
|
|
|
|
|
public async Task ValidKey_DisallowedPath_Returns403()
|
|
|
|
|
{
|
|
|
|
|
var disallowedPaths = new[] { "/api/admin/dashboard", "/api/users", "/health" };
|
|
|
|
|
|
|
|
|
|
foreach (var path in disallowedPaths)
|
|
|
|
|
{
|
|
|
|
|
var (middleware, context, nextCalled) = CreateMiddleware(ValidApiKey);
|
|
|
|
|
context.Request.Headers["X-Internal-Api-Key"] = ValidApiKey;
|
|
|
|
|
context.Request.Path = path;
|
|
|
|
|
|
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
|
|
|
|
|
|
nextCalled().Should().BeFalse($"valid key on disallowed path {path} should not call next");
|
|
|
|
|
context.Response.StatusCode.Should().Be(403);
|
test: bootstrap test infrastructure with critical coverage (QA-C1 through QA-C6)
QA-C1: Create xUnit test project, add to solution, wire dependencies
- api/tests/ProposalSystem.Tests with xUnit + FluentAssertions + NSubstitute
- InMemory EF Core provider for isolated DB tests
QA-C2: Proposal state machine transition tests (16 tests)
- Valid: InReview->Approved, Approved->Sent, Sent->Revised
- Invalid: InReview->Sent, Approved->Revised, Draft->Approved, etc.
- Edge cases: idempotency, missing line items, revision line item copying
- Audit and job publisher verification
QA-C3: Authorization attribute tests (16 tests)
- Controller-level [Authorize] on all controllers except AuthController
- Role requirements: admins/sysadmins on admin actions
- Dispatcher exclusion from admin/sysadmin routes
- SysAdmin-only user management enforcement
QA-C4: InternalApiKeyMiddleware tests (8 tests)
- Valid key sets claims and calls next
- Invalid key passes through to JWT (no 401/403)
- Missing key/empty config disables middleware
- Documents API-C1 vulnerability (key works on any path)
QA-C5: ProtectedRoute and RoleGuard tests (12 tests, vitest)
- ProtectedRoute: renders children when authenticated, redirects when not
- RoleGuard: role-based access, dispatcher/admin/sysadmin enforcement
- authSlice: setUser, logout, expired token handling
QA-C6: Lambda SQS handler tests (19 tests, pytest)
- pdf-generate: batch processing, failure reporting, malformed body
- suggestions: batch processing, proposal-not-found skip, AI item preservation
- API key caching, retry helpers
Total: 107 tests (76 .NET + 12 web + 19 Python), all passing.
2026-05-27 17:31:18 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static (InternalApiKeyMiddleware middleware, HttpContext context, Func<bool> nextCalled) CreateMiddleware(string? configuredKey)
|
|
|
|
|
{
|
|
|
|
|
var wasNextCalled = false;
|
|
|
|
|
RequestDelegate next = _ =>
|
|
|
|
|
{
|
|
|
|
|
wasNextCalled = true;
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var configData = new Dictionary<string, string?>();
|
|
|
|
|
if (configuredKey != null)
|
|
|
|
|
{
|
|
|
|
|
configData["INTERNAL_API_KEY"] = configuredKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var configuration = new ConfigurationBuilder()
|
|
|
|
|
.AddInMemoryCollection(configData)
|
|
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
var logger = Substitute.For<ILogger<InternalApiKeyMiddleware>>();
|
|
|
|
|
|
|
|
|
|
var middleware = new InternalApiKeyMiddleware(next, configuration, logger);
|
|
|
|
|
var context = new DefaultHttpContext();
|
|
|
|
|
|
|
|
|
|
return (middleware, context, () => wasNextCalled);
|
|
|
|
|
}
|
|
|
|
|
}
|