using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Novelly.Api.Users; namespace Novelly.Api.Tests; [TestFixture] public class ServiceApiKeyTests { private const string ConfiguredKey = "s3rvice-key-value"; private TestDatabase _db = null!; [SetUp] public void SetUp() => _db = new TestDatabase(); [TearDown] public void TearDown() => _db.Dispose(); [Test] public async Task A_request_with_the_configured_service_key_is_admitted_as_the_service_user() { await ServiceUser.EnsureSeededAsync(_db.Context, ConfiguredKey, NullLogger.Instance); var result = await AuthenticateAsync(ConfiguredKey, ConfiguredKey); Assert.Multiple(() => { Assert.That(result.Succeeded, Is.True); Assert.That(result.Principal?.FindFirstValue(ClaimTypes.NameIdentifier), Is.EqualTo(ServiceUser.Id.ToString())); Assert.That(result.Principal?.FindFirstValue(ClaimTypes.Role), Is.EqualTo(nameof(GlobalRole.Admin))); }); } [Test] public async Task A_request_with_a_wrong_key_is_rejected() { await ServiceUser.EnsureSeededAsync(_db.Context, ConfiguredKey, NullLogger.Instance); var result = await AuthenticateAsync(ConfiguredKey, "not-the-key"); Assert.Multiple(() => { Assert.That(result.Succeeded, Is.False); Assert.That(result.Principal, Is.Null); }); } [Test] public async Task The_api_still_serves_signed_in_users_when_no_service_key_is_configured() { var seeded = await ServiceUser.EnsureSeededAsync(_db.Context, null, NullLogger.Instance); var signedIn = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())], "Identity.Application")); var context = new DefaultHttpContext { User = signedIn }; var result = await AuthenticateAsync(configuredKey: null, presentedKey: null, context); Assert.Multiple(() => { Assert.That(seeded, Is.Null); Assert.That(result.None, Is.True); Assert.That(context.User, Is.SameAs(signedIn)); }); } [Test] public async Task A_service_key_presented_when_none_is_configured_is_rejected() { var result = await AuthenticateAsync(configuredKey: null, presentedKey: "anything"); Assert.That(result.Succeeded, Is.False); } [Test] public async Task Seeding_the_service_user_twice_leaves_one_row() { await ServiceUser.EnsureSeededAsync(_db.Context, ConfiguredKey, NullLogger.Instance); await ServiceUser.EnsureSeededAsync(_db.Context, ConfiguredKey, NullLogger.Instance); Assert.That(_db.Context.Users.Count(u => u.Id == ServiceUser.Id), Is.EqualTo(1)); } private async Task AuthenticateAsync(string? configuredKey, string? presentedKey, DefaultHttpContext? context = null) { var settings = configuredKey is null ? new Dictionary() : new Dictionary { [ServiceApiKeyAuthenticationHandler.ConfigurationKey] = configuredKey }; var configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build(); var handler = new ServiceApiKeyAuthenticationHandler( new StaticSchemeOptions(), NullLoggerFactory.Instance, UrlEncoder.Default, configuration, _db.Context); var httpContext = context ?? new DefaultHttpContext(); if (presentedKey is not null) { httpContext.Request.Headers[ServiceApiKeyAuthenticationHandler.HeaderName] = presentedKey; } var scheme = new AuthenticationScheme( ServiceApiKeyAuthenticationHandler.SchemeName, null, typeof(ServiceApiKeyAuthenticationHandler)); await handler.InitializeAsync(scheme, httpContext); return await handler.AuthenticateAsync(); } private class StaticSchemeOptions : IOptionsMonitor { public AuthenticationSchemeOptions CurrentValue { get; } = new(); public AuthenticationSchemeOptions Get(string? name) => CurrentValue; public IDisposable? OnChange(Action listener) => null; } }