282 lines
11 KiB
C#
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
|
|
}
|