// -----------------------------------------------------------------------------
// TestManifestRepository.cs
// Purpose: Test-only in-memory implementation of Storage.Repositories.IScanManifestRepository
// -----------------------------------------------------------------------------
using System.Collections.Concurrent;
using StellaOps.Scanner.Storage.Entities;
using StellaOps.Scanner.Storage.Repositories;
namespace StellaOps.Scanner.WebService.Services;
///
/// In-memory implementation of IScanManifestRepository for testing.
///
public sealed class TestManifestRepository : StellaOps.Scanner.Storage.Repositories.IScanManifestRepository
{
private readonly ConcurrentDictionary _manifestsByScanId = new();
private readonly ConcurrentDictionary _manifestsByHash = new(StringComparer.OrdinalIgnoreCase);
public Task GetByHashAsync(string manifestHash, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_manifestsByHash.TryGetValue(manifestHash, out var manifest) ? manifest : null);
}
public Task GetByScanIdAsync(Guid scanId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_manifestsByScanId.TryGetValue(scanId, out var manifest) ? manifest : null);
}
public Task SaveAsync(ScanManifestRow manifest, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(manifest);
cancellationToken.ThrowIfCancellationRequested();
_manifestsByScanId[manifest.ScanId] = manifest;
_manifestsByHash[manifest.ManifestHash] = manifest;
return Task.FromResult(manifest);
}
public Task MarkCompletedAsync(Guid manifestId, DateTimeOffset completedAt, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var manifest in _manifestsByScanId.Values)
{
if (manifest.ManifestId == manifestId)
{
manifest.ScanCompletedAt = completedAt;
break;
}
}
return Task.CompletedTask;
}
}
///
/// In-memory implementation of IProofBundleRepository for testing.
///
public sealed class TestProofBundleRepository : StellaOps.Scanner.Storage.Repositories.IProofBundleRepository
{
private readonly ConcurrentDictionary _bundlesByRootHash = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary> _bundlesByScanId = new();
public Task GetByRootHashAsync(string rootHash, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(_bundlesByRootHash.TryGetValue(rootHash, out var bundle) ? bundle : null);
}
public Task> GetByScanIdAsync(Guid scanId, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (_bundlesByScanId.TryGetValue(scanId, out var bundles))
{
return Task.FromResult>(bundles.ToList());
}
return Task.FromResult>(Array.Empty());
}
public Task SaveAsync(ProofBundleRow bundle, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(bundle);
cancellationToken.ThrowIfCancellationRequested();
_bundlesByRootHash[bundle.RootHash] = bundle;
var scanBundles = _bundlesByScanId.GetOrAdd(bundle.ScanId, _ => new List());
lock (scanBundles)
{
// Replace existing if same root hash, otherwise add
var existingIndex = scanBundles.FindIndex(b => string.Equals(b.RootHash, bundle.RootHash, StringComparison.OrdinalIgnoreCase));
if (existingIndex >= 0)
{
scanBundles[existingIndex] = bundle;
}
else
{
scanBundles.Add(bundle);
}
}
return Task.FromResult(bundle);
}
public Task DeleteExpiredAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var now = DateTimeOffset.UtcNow;
var expired = _bundlesByRootHash.Values
.Where(b => b.ExpiresAt.HasValue && b.ExpiresAt.Value < now)
.ToList();
foreach (var bundle in expired)
{
_bundlesByRootHash.TryRemove(bundle.RootHash, out _);
if (_bundlesByScanId.TryGetValue(bundle.ScanId, out var scanBundles))
{
lock (scanBundles)
{
scanBundles.RemoveAll(b => string.Equals(b.RootHash, bundle.RootHash, StringComparison.OrdinalIgnoreCase));
}
}
}
return Task.FromResult(expired.Count);
}
}