Add new features and tests for AirGap and Time modules
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Introduced `SbomService` tasks documentation.
- Updated `StellaOps.sln` to include new projects: `StellaOps.AirGap.Time` and `StellaOps.AirGap.Importer`.
- Added unit tests for `BundleImportPlanner`, `DsseVerifier`, `ImportValidator`, and other components in the `StellaOps.AirGap.Importer.Tests` namespace.
- Implemented `InMemoryBundleRepositories` for testing bundle catalog and item repositories.
- Created `MerkleRootCalculator`, `RootRotationPolicy`, and `TufMetadataValidator` tests.
- Developed `StalenessCalculator` and `TimeAnchorLoader` tests in the `StellaOps.AirGap.Time.Tests` namespace.
- Added `fetch-sbomservice-deps.sh` script for offline dependency fetching.
This commit is contained in:
master
2025-11-20 23:29:54 +02:00
parent 65b1599229
commit 79b8e53441
182 changed files with 6660 additions and 1242 deletions

View File

@@ -0,0 +1,9 @@
using StellaOps.AirGap.Importer.Models;
namespace StellaOps.AirGap.Importer.Repositories;
public interface IBundleCatalogRepository
{
Task UpsertAsync(BundleCatalogEntry entry, CancellationToken cancellationToken);
Task<IReadOnlyList<BundleCatalogEntry>> ListAsync(string tenantId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,9 @@
using StellaOps.AirGap.Importer.Models;
namespace StellaOps.AirGap.Importer.Repositories;
public interface IBundleItemRepository
{
Task UpsertManyAsync(IEnumerable<BundleItem> items, CancellationToken cancellationToken);
Task<IReadOnlyList<BundleItem>> ListByBundleAsync(string tenantId, string bundleId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,63 @@
using StellaOps.AirGap.Importer.Models;
namespace StellaOps.AirGap.Importer.Repositories;
/// <summary>
/// Deterministic in-memory implementations suitable for offline tests and as a template for Mongo-backed repos.
/// Enforces tenant isolation and stable ordering (by BundleId then Path).
/// </summary>
public sealed class InMemoryBundleCatalogRepository : IBundleCatalogRepository
{
private readonly Dictionary<string, List<BundleCatalogEntry>> _catalog = new(StringComparer.Ordinal);
public Task UpsertAsync(BundleCatalogEntry entry, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var bucket = _catalog.GetValueOrDefault(entry.TenantId) ?? new List<BundleCatalogEntry>();
bucket.RemoveAll(e => e.BundleId == entry.BundleId);
bucket.Add(entry);
_catalog[entry.TenantId] = bucket;
return Task.CompletedTask;
}
public Task<IReadOnlyList<BundleCatalogEntry>> ListAsync(string tenantId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var items = _catalog.GetValueOrDefault(tenantId) ?? new List<BundleCatalogEntry>();
return Task.FromResult<IReadOnlyList<BundleCatalogEntry>>(items
.OrderBy(e => e.BundleId, StringComparer.Ordinal)
.ToList());
}
}
public sealed class InMemoryBundleItemRepository : IBundleItemRepository
{
private readonly Dictionary<(string TenantId, string BundleId), List<BundleItem>> _items = new();
public Task UpsertManyAsync(IEnumerable<BundleItem> items, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var item in items)
{
var key = (item.TenantId, item.BundleId);
if (!_items.TryGetValue(key, out var list))
{
list = new List<BundleItem>();
_items[key] = list;
}
list.RemoveAll(i => i.Path == item.Path);
list.Add(item);
}
return Task.CompletedTask;
}
public Task<IReadOnlyList<BundleItem>> ListByBundleAsync(string tenantId, string bundleId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var key = (tenantId, bundleId);
var list = _items.GetValueOrDefault(key) ?? new List<BundleItem>();
return Task.FromResult<IReadOnlyList<BundleItem>>(list
.OrderBy(i => i.Path, StringComparer.Ordinal)
.ToList());
}
}