using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Signals.Models; using StellaOps.Signals.Persistence; using StellaOps.Signals.Services; using Xunit; namespace StellaOps.Signals.Tests; public class UnknownsIngestionServiceTests { [Fact] public async Task IngestAsync_StoresNormalizedUnknowns() { var repo = new InMemoryUnknownsRepository(); var service = new UnknownsIngestionService(repo, TimeProvider.System, NullLogger.Instance); var request = new UnknownsIngestRequest { Subject = new ReachabilitySubject { Component = "demo", Version = "1.0.0" }, CallgraphId = "cg-1", Unknowns = new List { new() { SymbolId = "symA", Purl = "pkg:pypi/foo", Reason = "missing-edge" }, new() // empty entry should be ignored } }; var response = await service.IngestAsync(request, CancellationToken.None); Assert.Equal("demo|1.0.0", response.SubjectKey); Assert.Equal(1, response.UnknownsCount); Assert.Single(repo.Stored); Assert.Equal("symA", repo.Stored[0].SymbolId); Assert.Equal("pkg:pypi/foo", repo.Stored[0].Purl); } [Fact] public async Task IngestAsync_ThrowsWhenEmpty() { var repo = new InMemoryUnknownsRepository(); var service = new UnknownsIngestionService(repo, TimeProvider.System, NullLogger.Instance); var request = new UnknownsIngestRequest { Subject = new ReachabilitySubject { Component = "demo", Version = "1.0.0" }, CallgraphId = "cg-1", Unknowns = new List() }; await Assert.ThrowsAsync(() => service.IngestAsync(request, CancellationToken.None)); } private sealed class InMemoryUnknownsRepository : IUnknownsRepository { public List Stored { get; } = new(); public Task UpsertAsync(string subjectKey, IEnumerable items, CancellationToken cancellationToken) { Stored.Clear(); Stored.AddRange(items); return Task.CompletedTask; } public Task> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken) { return Task.FromResult((IReadOnlyList)Stored); } public Task CountBySubjectAsync(string subjectKey, CancellationToken cancellationToken) { return Task.FromResult(Stored.Count); } } }