using GB5Shared.Connection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using PartnerSL.Middleware;
using PartnerSL.Services;
using Xunit;
namespace PartnerTests;
///
/// Covers PartnerSL.Middleware.ApiKeyAuthMiddleware — specifically the fail-open bug this test
/// project was created to close (Thread 7 of the plan tracker). No live SQL Server is available
/// in this environment, so only paths reachable without an actual TPARTNERAPIKEY row are covered
/// here: the no-header pass-through, the internal-path skip, and — the one that matters most —
/// the DB-lookup-throws case. That last one is fully testable without a live DB because
/// IApplicationConnection.Gb5SystemConnectionString() is itself mockable to throw, which is
/// exactly the first thing InvokeAsync awaits inside its try block.
///
public class ApiKeyAuthMiddlewareTests
{
private static DomainCacheService BuildDomainCacheService()
{
// TryResolve (the only method this middleware calls) reads straight from the injected
// IMemoryCache and never touches _scopeFactory — a real cache + an unexercised
// IServiceScopeFactory mock is enough, mirroring this repo's established "not exercised,
// trivial mock" pattern (e.g. ClientAuthBLLTests' EventLogPublish).
return new DomainCacheService(
new Mock().Object,
new MemoryCache(new MemoryCacheOptions()),
NullLogger.Instance);
}
private static HttpContext BuildHttpContext(string? apiKeyHeader, string path = "/partner/Something")
{
var context = new DefaultHttpContext();
context.Request.Path = path;
if (apiKeyHeader is not null)
context.Request.Headers["X-Api-Key"] = apiKeyHeader;
return context;
}
[Fact]
public async Task Test_NoApiKeyHeader_PassesThrough_NeverTouchesDb()
{
var appConnection = new Mock();
var nextCalled = false;
RequestDelegate next = _ => { nextCalled = true; return Task.CompletedTask; };
var middleware = new ApiKeyAuthMiddleware(next, NullLogger.Instance);
var context = BuildHttpContext(apiKeyHeader: null);
await middleware.InvokeAsync(context, appConnection.Object, BuildDomainCacheService());
Assert.True(nextCalled);
Assert.Equal(200, context.Response.StatusCode); // DefaultHttpContext's untouched default
appConnection.Verify(a => a.Gb5SystemConnectionString(), Times.Never);
}
[Theory]
[InlineData("/dapr/subscribe")]
[InlineData("/swagger/index.html")]
[InlineData("/healthz")]
[InlineData("/")]
public async Task Test_InternalPath_PassesThrough_EvenWithApiKeyHeaderPresent(string internalPath)
{
var appConnection = new Mock();
var nextCalled = false;
RequestDelegate next = _ => { nextCalled = true; return Task.CompletedTask; };
var middleware = new ApiKeyAuthMiddleware(next, NullLogger.Instance);
var context = BuildHttpContext(apiKeyHeader: "some-key-value", path: internalPath);
await middleware.InvokeAsync(context, appConnection.Object, BuildDomainCacheService());
Assert.True(nextCalled);
appConnection.Verify(a => a.Gb5SystemConnectionString(), Times.Never);
}
[Fact]
public async Task Test_DbLookupThrows_RejectsWithUnauthorized_DoesNotCallNext()
{
// The actual regression test: before the fix, this exact scenario (Gb5SystemConnectionString
// throwing — standing in for any DB/Vault outage during the API-key lookup) resulted in the
// middleware logging a warning and calling next() anyway, silently authenticating an
// unverified request. It must now reject with 401 and never call next().
var appConnection = new Mock();
appConnection.Setup(a => a.Gb5SystemConnectionString())
.ThrowsAsync(new InvalidOperationException("simulated DB/Vault outage"));
var nextCalled = false;
RequestDelegate next = _ => { nextCalled = true; return Task.CompletedTask; };
var middleware = new ApiKeyAuthMiddleware(next, NullLogger.Instance);
var context = BuildHttpContext(apiKeyHeader: "some-key-value");
context.Response.Body = new MemoryStream();
await middleware.InvokeAsync(context, appConnection.Object, BuildDomainCacheService());
Assert.False(nextCalled);
Assert.Equal(401, context.Response.StatusCode);
}
}