Adds 2 units and docs for unit 3. nfr-requirements plan

This commit is contained in:
2026-06-29 22:18:37 +02:00
parent 0e01ca1e1c
commit c156107cb1
126 changed files with 15204 additions and 80199 deletions
@@ -0,0 +1,240 @@
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<IMasterRegistrationRepository>();
_protector = Substitute.For<IMasterApiKeyProtector>();
var deps = new MasterAvailabilityServiceDependencies(
_repo,
_protector,
NullLogger<MasterAvailabilityService>.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<MasterRegistration>(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();
}
}