Files
scadalink-design/tests/ScadaLink.ConfigurationDatabase.Tests/InboundApiRepositoryTests.cs

153 lines
5.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.ConfigurationDatabase;
using ScadaLink.ConfigurationDatabase.Repositories;
namespace ScadaLink.ConfigurationDatabase.Tests;
public class InboundApiRepositoryTests : IDisposable
{
private readonly ScadaLinkDbContext _context;
private readonly CapturingLogger<InboundApiRepository> _logger = new();
private readonly InboundApiRepository _repository;
public InboundApiRepositoryTests()
{
_context = SqliteTestHelper.CreateInMemoryContext();
_repository = new InboundApiRepository(_context, _logger);
}
public void Dispose()
{
_context.Database.CloseConnection();
_context.Dispose();
}
[Fact]
public async Task AddApiKey_AndGetById_RoundTrips()
{
var key = new ApiKey("Key1", "secret-value-1") { IsEnabled = true };
await _repository.AddApiKeyAsync(key);
await _repository.SaveChangesAsync();
var loaded = await _repository.GetApiKeyByIdAsync(key.Id);
Assert.NotNull(loaded);
Assert.Equal("Key1", loaded!.Name);
var byValue = await _repository.GetApiKeyByValueAsync("secret-value-1");
Assert.NotNull(byValue);
Assert.Equal(key.Id, byValue!.Id);
}
[Fact]
public async Task AddApiMethod_AndGetByName_RoundTrips()
{
var method = new ApiMethod("DoThing", "return 1;");
await _repository.AddApiMethodAsync(method);
await _repository.SaveChangesAsync();
var loaded = await _repository.GetMethodByNameAsync("DoThing");
Assert.NotNull(loaded);
Assert.Equal(method.Id, loaded!.Id);
}
[Fact]
public async Task GetApprovedKeysForMethod_WithValidCsv_ReturnsAllKeys()
{
var k1 = new ApiKey("K1", "v1");
var k2 = new ApiKey("K2", "v2");
await _repository.AddApiKeyAsync(k1);
await _repository.AddApiKeyAsync(k2);
await _repository.SaveChangesAsync();
var method = new ApiMethod("M", "return 1;") { ApprovedApiKeyIds = $"{k1.Id}, {k2.Id}" };
await _repository.AddApiMethodAsync(method);
await _repository.SaveChangesAsync();
var keys = await _repository.GetApprovedKeysForMethodAsync(method.Id);
Assert.Equal(2, keys.Count);
Assert.Empty(_logger.Warnings);
}
[Fact]
public async Task GetApprovedKeysForMethod_WithMalformedCsvToken_LogsWarningAndDropsToken()
{
// Regression guard for ConfigurationDatabase-008: a corrupt token (a name where an
// integer id is expected) must not be dropped silently — the corruption must be
// observable via a logged warning, while the valid ids still resolve.
var k1 = new ApiKey("K1", "v1");
await _repository.AddApiKeyAsync(k1);
await _repository.SaveChangesAsync();
var method = new ApiMethod("M", "return 1;") { ApprovedApiKeyIds = $"{k1.Id},not-an-id" };
await _repository.AddApiMethodAsync(method);
await _repository.SaveChangesAsync();
var keys = await _repository.GetApprovedKeysForMethodAsync(method.Id);
Assert.Single(keys);
Assert.Equal(k1.Id, keys[0].Id);
Assert.Single(_logger.Warnings);
Assert.Contains("not-an-id", _logger.Warnings[0]);
}
[Fact]
public async Task GetApprovedKeysForMethod_WithNullOrEmptyCsv_ReturnsEmptyWithoutWarning()
{
var method = new ApiMethod("M", "return 1;");
await _repository.AddApiMethodAsync(method);
await _repository.SaveChangesAsync();
var keys = await _repository.GetApprovedKeysForMethodAsync(method.Id);
Assert.Empty(keys);
Assert.Empty(_logger.Warnings);
}
[Fact]
public async Task DeleteApiMethod_RemovesEntity()
{
var method = new ApiMethod("ToDelete", "return 1;");
await _repository.AddApiMethodAsync(method);
await _repository.SaveChangesAsync();
await _repository.DeleteApiMethodAsync(method.Id);
await _repository.SaveChangesAsync();
Assert.Null(await _repository.GetApiMethodByIdAsync(method.Id));
}
[Fact]
public void Constructor_NullContext_Throws()
{
Assert.Throws<ArgumentNullException>(() => new InboundApiRepository(null!));
}
}
/// <summary>Minimal ILogger that captures warning-level messages for assertions.</summary>
internal sealed class CapturingLogger<T> : ILogger<T>
{
public List<string> Warnings { get; } = new();
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (logLevel == LogLevel.Warning)
{
Warnings.Add(formatter(state, exception));
}
}
private sealed class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
public void Dispose() { }
}
}