Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
+240
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user