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 _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(() => new InboundApiRepository(null!)); } } /// Minimal ILogger that captures warning-level messages for assertions. internal sealed class CapturingLogger : ILogger { public List Warnings { get; } = new(); public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (logLevel == LogLevel.Warning) { Warnings.Add(formatter(state, exception)); } } private sealed class NullScope : IDisposable { public static readonly NullScope Instance = new(); public void Dispose() { } } }