Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/Index/OfflineBuildIdIndexTests.cs
2026-01-09 18:27:46 +02:00

282 lines
11 KiB
C#

using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace StellaOps.Scanner.Analyzers.Native.Index.Tests;
/// <summary>
/// Unit tests for <see cref="OfflineBuildIdIndex"/>.
/// </summary>
public sealed class OfflineBuildIdIndexTests : IDisposable
{
private readonly string _tempDir;
public OfflineBuildIdIndexTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"buildid-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
#region Loading Tests
[Fact]
public async Task LoadAsync_EmptyIndex_WhenNoPathConfigured()
{
var options = Options.Create(new BuildIdIndexOptions { IndexPath = null });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.True(index.IsLoaded);
Assert.Equal(0, index.Count);
}
[Fact]
public async Task LoadAsync_EmptyIndex_WhenFileNotFound()
{
var options = Options.Create(new BuildIdIndexOptions { IndexPath = "/nonexistent/file.ndjson" });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.True(index.IsLoaded);
Assert.Equal(0, index.Count);
}
[Fact]
public async Task LoadAsync_ParsesNdjsonEntries()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"}
{"build_id":"pe-cv:12345678-1234-1234-1234-123456789012-1","purl":"pkg:nuget/System.Text.Json@8.0.0","confidence":"inferred"}
{"build_id":"macho-uuid:fedcba9876543210fedcba9876543210","purl":"pkg:brew/openssl@3.0.0","distro":"macos","confidence":"exact"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.True(index.IsLoaded);
Assert.Equal(3, index.Count);
}
[Fact]
public async Task LoadAsync_SkipsEmptyLines()
{
var indexPath = Path.Combine(_tempDir, "index-empty-lines.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
{"build_id":"gnu-build-id:def456","purl":"pkg:deb/debian/libssl@1.1"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.Equal(2, index.Count);
}
[Fact]
public async Task LoadAsync_SkipsCommentLines()
{
var indexPath = Path.Combine(_tempDir, "index-comments.ndjson");
await File.WriteAllTextAsync(indexPath, """
# This is a comment
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
// Another comment style
{"build_id":"gnu-build-id:def456","purl":"pkg:deb/debian/libssl@1.1"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.Equal(2, index.Count);
}
[Fact]
public async Task LoadAsync_SkipsInvalidJsonLines()
{
var indexPath = Path.Combine(_tempDir, "index-invalid.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
not valid json at all
{"build_id":"gnu-build-id:def456","purl":"pkg:deb/debian/libssl@1.1"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.Equal(2, index.Count);
}
#endregion
#region Lookup Tests
[Fact]
public async Task LookupAsync_ReturnsNull_WhenNotFound()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
var result = await index.LookupAsync("gnu-build-id:notfound");
Assert.Null(result);
}
[Fact]
public async Task LookupAsync_ReturnsNull_ForNullOrEmpty()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:abc123","purl":"pkg:deb/debian/libc6@2.31"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
Assert.Null(await index.LookupAsync(null!));
Assert.Null(await index.LookupAsync(""));
Assert.Null(await index.LookupAsync(" "));
}
[Fact]
public async Task LookupAsync_FindsExactMatch()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:abc123def456","purl":"pkg:deb/debian/libc6@2.31","version":"2.31","distro":"debian","confidence":"exact","indexed_at":"2025-01-15T10:00:00Z"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
var result = await index.LookupAsync("gnu-build-id:abc123def456");
Assert.NotNull(result);
Assert.Equal("gnu-build-id:abc123def456", result.BuildId);
Assert.Equal("pkg:deb/debian/libc6@2.31", result.Purl);
Assert.Equal("2.31", result.Version);
Assert.Equal("debian", result.SourceDistro);
Assert.Equal(BuildIdConfidence.Exact, result.Confidence);
}
[Fact]
public async Task LookupAsync_CaseInsensitive()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:ABC123DEF456","purl":"pkg:deb/debian/libc6@2.31"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
// Query with lowercase
var result = await index.LookupAsync("gnu-build-id:abc123def456");
Assert.NotNull(result);
Assert.Equal("pkg:deb/debian/libc6@2.31", result.Purl);
}
#endregion
#region Batch Lookup Tests
[Fact]
public async Task BatchLookupAsync_ReturnsFoundEntries()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:aaa","purl":"pkg:deb/debian/liba@1.0"}
{"build_id":"gnu-build-id:bbb","purl":"pkg:deb/debian/libb@1.0"}
{"build_id":"gnu-build-id:ccc","purl":"pkg:deb/debian/libc@1.0"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
var results = await index.BatchLookupAsync(["gnu-build-id:aaa", "gnu-build-id:notfound", "gnu-build-id:ccc"]);
Assert.Equal(2, results.Count);
Assert.Contains(results, r => r.Purl == "pkg:deb/debian/liba@1.0");
Assert.Contains(results, r => r.Purl == "pkg:deb/debian/libc@1.0");
}
[Fact]
public async Task BatchLookupAsync_SkipsNullAndEmpty()
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
await File.WriteAllTextAsync(indexPath, """
{"build_id":"gnu-build-id:aaa","purl":"pkg:deb/debian/liba@1.0"}
""");
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
var results = await index.BatchLookupAsync([null!, "", " ", "gnu-build-id:aaa"]);
Assert.Single(results);
Assert.Equal("pkg:deb/debian/liba@1.0", results[0].Purl);
}
#endregion
#region Confidence Parsing Tests
[Theory]
[InlineData("exact", BuildIdConfidence.Exact)]
[InlineData("EXACT", BuildIdConfidence.Exact)]
[InlineData("inferred", BuildIdConfidence.Inferred)]
[InlineData("Inferred", BuildIdConfidence.Inferred)]
[InlineData("heuristic", BuildIdConfidence.Heuristic)]
[InlineData("unknown", BuildIdConfidence.Heuristic)] // Defaults to heuristic
[InlineData("", BuildIdConfidence.Heuristic)]
public async Task LoadAsync_ParsesConfidenceLevels(string confidenceValue, BuildIdConfidence expected)
{
var indexPath = Path.Combine(_tempDir, "index.ndjson");
var entry = new { build_id = "gnu-build-id:test", purl = "pkg:test/test@1.0", confidence = confidenceValue };
await File.WriteAllTextAsync(indexPath, JsonSerializer.Serialize(entry));
var options = Options.Create(new BuildIdIndexOptions { IndexPath = indexPath, RequireSignature = false });
var index = new OfflineBuildIdIndex(options, NullLogger<OfflineBuildIdIndex>.Instance);
await index.LoadAsync();
var result = await index.LookupAsync("gnu-build-id:test");
Assert.NotNull(result);
Assert.Equal(expected, result.Confidence);
}
#endregion
}