using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using SlpModularCms.Modules.Availability.Data.Entities; using SlpModularCms.Modules.Availability.Repositories; using SlpModularCms.Modules.Availability.Services; namespace SlpModularCms.Modules.Availability.Tests.Services; public class MasterAvailabilityServiceTests : IDisposable { private readonly IMasterRegistrationRepository _repo; private readonly IMasterApiKeyProtector _protector; private readonly MasterAvailabilityService _svc; public MasterAvailabilityServiceTests() { _repo = Substitute.For(); _protector = Substitute.For(); var deps = new MasterAvailabilityServiceDependencies( _repo, _protector, NullLogger.Instance); _svc = new MasterAvailabilityService(deps); MasterAvailabilityService.ResetStaticCacheForTest(); } public void Dispose() => MasterAvailabilityService.ResetStaticCacheForTest(); // --- RegisterAsync --- [Fact] public async Task RegisterAsync_CreatesNewRegistration_WhenNoneExists() { _repo.GetAsync().Returns((MasterRegistration?)null); _protector.Protect("key123").Returns("enc:key123"); var result = await _svc.RegisterAsync("https://master.example.com", "key123"); result.Should().BeTrue(); await _repo.Received(1).AddAsync(Arg.Is(r => r.MasterUrl == "https://master.example.com" && r.ApiKey == "enc:key123" && r.Id == MasterRegistration.SingletonId)); await _repo.Received(1).SaveChangesAsync(); } [Fact] public async Task RegisterAsync_ReturnsTrue_OnReregistrationWithMatchingKey() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://old.example.com", ApiKey = "enc:key123", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key123").Returns("key123"); var result = await _svc.RegisterAsync("https://new.example.com", "key123"); result.Should().BeTrue(); existing.MasterUrl.Should().Be("https://new.example.com"); _repo.Received(1).Update(existing); await _repo.Received(1).SaveChangesAsync(); } [Fact] public async Task RegisterAsync_ReturnsFalse_WhenKeyMismatch() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "enc:key123", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key123").Returns("key123"); var result = await _svc.RegisterAsync("https://master.example.com", "wrong-key"); result.Should().BeFalse(); await _repo.DidNotReceive().SaveChangesAsync(); } [Fact] public async Task RegisterAsync_ReturnsFalse_WhenDecryptionFails() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "corrupt", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("corrupt").Returns((string?)null); var result = await _svc.RegisterAsync("https://master.example.com", "key123"); result.Should().BeFalse(); } // --- PushStatusAsync --- [Fact] public async Task PushStatusAsync_UpdatesStaticCache_WhenKeyValid() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "enc:key", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key").Returns("key"); var result = await _svc.PushStatusAsync("key", false, "Under maintenance"); result.Should().BeTrue(); var status = _svc.GetMasterStatus(); status.IsAvailable.Should().BeFalse(); status.DisableMessage.Should().Be("Under maintenance"); } [Fact] public async Task PushStatusAsync_ReturnsFalse_WhenNoRegistration() { _repo.GetAsync().Returns((MasterRegistration?)null); var result = await _svc.PushStatusAsync("key", false, null); result.Should().BeFalse(); await _repo.DidNotReceive().SaveChangesAsync(); } [Fact] public async Task PushStatusAsync_ReturnsFalse_WhenKeyMismatch() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "enc:key", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key").Returns("correct-key"); var result = await _svc.PushStatusAsync("wrong-key", false, null); result.Should().BeFalse(); } // --- GetRegisteredUrlAsync --- [Fact] public async Task GetRegisteredUrlAsync_ReturnsMasterUrl_WhenKeyValid() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "enc:key", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key").Returns("key"); var result = await _svc.GetRegisteredUrlAsync("key"); result.Should().Be("https://master.example.com"); _repo.Received(1).Update(existing); await _repo.Received(1).SaveChangesAsync(); } [Fact] public async Task GetRegisteredUrlAsync_ReturnsNull_WhenNoRegistration() { _repo.GetAsync().Returns((MasterRegistration?)null); var result = await _svc.GetRegisteredUrlAsync("key"); result.Should().BeNull(); } [Fact] public async Task GetRegisteredUrlAsync_ReturnsNull_WhenKeyMismatch() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "enc:key", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key").Returns("correct-key"); var result = await _svc.GetRegisteredUrlAsync("wrong-key"); result.Should().BeNull(); } // --- GetMasterStatus --- [Fact] public async Task GetMasterStatus_ReflectsLastPushedStatus() { var existing = new MasterRegistration { Id = MasterRegistration.SingletonId, MasterUrl = "https://master.example.com", ApiKey = "enc:key", RegisteredAt = DateTimeOffset.UtcNow }; _repo.GetAsync().Returns(existing); _protector.Unprotect("enc:key").Returns("key"); await _svc.PushStatusAsync("key", false, "Down for maintenance"); var status = _svc.GetMasterStatus(); status.IsAvailable.Should().BeFalse(); status.DisableMessage.Should().Be("Down for maintenance"); } [Fact] public void GetMasterStatus_ReturnsAvailableByDefault() { var status = _svc.GetMasterStatus(); status.IsAvailable.Should().BeTrue(); status.DisableMessage.Should().BeNull(); } }