using ErsatzTV.Application.Auth; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Tests.Support; using LanguageExt; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Tests.Application.Auth; [TestFixture] public class ClaimLocalAdminHandlerTests { private InMemoryTvContext _db = null!; private IConfigElementRepository _configElementRepository = null!; private ILocalPasswordHasher _passwordHasher = null!; [SetUp] public async Task SetUp() { _db = await InMemoryTvContext.CreateAsync(); _configElementRepository = new ConfigElementRepository(_db.Factory); _passwordHasher = new LocalPasswordHasher(); } [TearDown] public async Task TearDown() => await _db.DisposeAsync(); private ClaimLocalAdminHandler MakeHandler() => new(_db.Factory, _passwordHasher); [Test] public async Task Handle_Should_Claim_Fresh_Admin_And_Persist_All_Config_Elements() { ClaimLocalAdminHandler handler = MakeHandler(); Either result = await handler.Handle( new ClaimLocalAdmin("Operator", "supersecret"), CancellationToken.None); result.IsRight.ShouldBeTrue(); LocalAdminPrincipal principal = result.Match( Left: e => throw new ShouldAssertException(e.ToString()), Right: p => p); principal.Username.ShouldBe("Operator"); principal.SecurityStamp.ShouldNotBeNullOrEmpty(); Option storedUser = await _configElementRepository.GetValue( ConfigElementKey.AuthLocalAdminUsername, CancellationToken.None); storedUser.IfNone("").ShouldBe("Operator"); Option storedStamp = await _configElementRepository.GetValue( ConfigElementKey.AuthSecurityStamp, CancellationToken.None); storedStamp.IfNone("").ShouldBe(principal.SecurityStamp); Option storedHash = await _configElementRepository.GetValue( ConfigElementKey.AuthLocalAdminPasswordHash, CancellationToken.None); storedHash.IsSome.ShouldBeTrue(); _passwordHasher.Verify(storedHash.IfNone(""), "supersecret") .ShouldNotBe(LocalPasswordVerification.Failed); } [Test] public async Task Handle_Should_Refuse_Second_Claim_When_Admin_Already_Configured() { ClaimLocalAdminHandler handler = MakeHandler(); (await handler.Handle(new ClaimLocalAdmin("First", "supersecret"), CancellationToken.None)) .IsRight.ShouldBeTrue(); Either result = await handler.Handle( new ClaimLocalAdmin("Second", "anothersecret"), CancellationToken.None); result.IsLeft.ShouldBeTrue(); // The original credential is untouched. Option storedUser = await _configElementRepository.GetValue( ConfigElementKey.AuthLocalAdminUsername, CancellationToken.None); storedUser.IfNone("").ShouldBe("First"); } [Test] public async Task Handle_Should_Reject_Whitespace_Username_And_Persist_Nothing() { ClaimLocalAdminHandler handler = MakeHandler(); Either result = await handler.Handle( new ClaimLocalAdmin(" ", "supersecret"), CancellationToken.None); result.IsLeft.ShouldBeTrue(); await AssertNothingPersisted(); } [Test] public async Task Handle_Should_Reject_Short_Password_And_Persist_Nothing() { string shortPassword = new('a', AuthConstants.MinPasswordLength - 1); ClaimLocalAdminHandler handler = MakeHandler(); Either result = await handler.Handle( new ClaimLocalAdmin("Operator", shortPassword), CancellationToken.None); result.IsLeft.ShouldBeTrue(); await AssertNothingPersisted(); } [Test] public async Task Handle_Should_Reject_Over_Long_Password_And_Persist_Nothing() { // LocalAdminHelpers.MaxPasswordLength (1024) is internal; use the documented bound directly. string longPassword = new('a', 1024 + 1); ClaimLocalAdminHandler handler = MakeHandler(); Either result = await handler.Handle( new ClaimLocalAdmin("Operator", longPassword), CancellationToken.None); result.IsLeft.ShouldBeTrue(); await AssertNothingPersisted(); } private async Task AssertNothingPersisted() { (await _configElementRepository.GetConfigElement( ConfigElementKey.AuthLocalAdminUsername, CancellationToken.None)).IsNone.ShouldBeTrue(); (await _configElementRepository.GetConfigElement( ConfigElementKey.AuthLocalAdminPasswordHash, CancellationToken.None)).IsNone.ShouldBeTrue(); (await _configElementRepository.GetConfigElement( ConfigElementKey.AuthSecurityStamp, CancellationToken.None)).IsNone.ShouldBeTrue(); } }