feat(telemetry): add telemetry client and services for tracking events
- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint. - Created TtfsTelemetryService for emitting specific telemetry events related to TTFS. - Added tests for TelemetryClient to ensure event queuing and flushing functionality. - Introduced models for reachability drift detection, including DriftResult and DriftedSink. - Developed DriftApiService for interacting with the drift detection API. - Updated FirstSignalCardComponent to emit telemetry events on signal appearance. - Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Native.Index;
|
||||
using StellaOps.Scanner.Emit.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Tests.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="NativePurlBuilder"/>.
|
||||
/// Sprint: SPRINT_3500_0012_0001
|
||||
/// Task: BSE-008
|
||||
/// </summary>
|
||||
public sealed class NativePurlBuilderTests
|
||||
{
|
||||
private readonly NativePurlBuilder _builder = new();
|
||||
|
||||
#region FromIndexResult Tests
|
||||
|
||||
[Fact]
|
||||
public void FromIndexResult_ReturnsPurlFromResult()
|
||||
{
|
||||
var result = new BuildIdLookupResult(
|
||||
BuildId: "gnu-build-id:abc123",
|
||||
Purl: "pkg:deb/debian/libc6@2.31",
|
||||
Version: "2.31",
|
||||
SourceDistro: "debian",
|
||||
Confidence: BuildIdConfidence.Exact,
|
||||
IndexedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
var purl = _builder.FromIndexResult(result);
|
||||
|
||||
Assert.Equal("pkg:deb/debian/libc6@2.31", purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromIndexResult_ThrowsForNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _builder.FromIndexResult(null!));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FromUnresolvedBinary Tests
|
||||
|
||||
[Fact]
|
||||
public void FromUnresolvedBinary_GeneratesGenericPurl()
|
||||
{
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libssl.so.3"
|
||||
};
|
||||
|
||||
var purl = _builder.FromUnresolvedBinary(metadata);
|
||||
|
||||
Assert.StartsWith("pkg:generic/libssl.so.3@unknown", purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromUnresolvedBinary_IncludesBuildId()
|
||||
{
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libssl.so.3",
|
||||
BuildId = "gnu-build-id:abc123def456"
|
||||
};
|
||||
|
||||
var purl = _builder.FromUnresolvedBinary(metadata);
|
||||
|
||||
Assert.Contains("build-id=gnu-build-id%3Aabc123def456", purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromUnresolvedBinary_IncludesArchitecture()
|
||||
{
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libssl.so.3",
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
var purl = _builder.FromUnresolvedBinary(metadata);
|
||||
|
||||
Assert.Contains("arch=x86_64", purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromUnresolvedBinary_IncludesPlatform()
|
||||
{
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libssl.so.3",
|
||||
Platform = "linux"
|
||||
};
|
||||
|
||||
var purl = _builder.FromUnresolvedBinary(metadata);
|
||||
|
||||
Assert.Contains("os=linux", purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromUnresolvedBinary_SortsQualifiersAlphabetically()
|
||||
{
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libssl.so.3",
|
||||
BuildId = "gnu-build-id:abc",
|
||||
Architecture = "x86_64",
|
||||
Platform = "linux"
|
||||
};
|
||||
|
||||
var purl = _builder.FromUnresolvedBinary(metadata);
|
||||
|
||||
// arch < build-id < os (alphabetical)
|
||||
var archIndex = purl.IndexOf("arch=", StringComparison.Ordinal);
|
||||
var buildIdIndex = purl.IndexOf("build-id=", StringComparison.Ordinal);
|
||||
var osIndex = purl.IndexOf("os=", StringComparison.Ordinal);
|
||||
|
||||
Assert.True(archIndex < buildIdIndex);
|
||||
Assert.True(buildIdIndex < osIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FromDistroPackage Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData("deb", "debian", "pkg:deb/debian/libc6@2.31")]
|
||||
[InlineData("debian", "debian", "pkg:deb/debian/libc6@2.31")]
|
||||
[InlineData("ubuntu", "ubuntu", "pkg:deb/ubuntu/libc6@2.31")]
|
||||
[InlineData("rpm", "fedora", "pkg:rpm/fedora/libc6@2.31")]
|
||||
[InlineData("apk", "alpine", "pkg:apk/alpine/libc6@2.31")]
|
||||
[InlineData("pacman", "arch", "pkg:pacman/arch/libc6@2.31")]
|
||||
public void FromDistroPackage_MapsDistroToPurlType(string distro, string distroName, string expectedPrefix)
|
||||
{
|
||||
var purl = _builder.FromDistroPackage(distro, distroName, "libc6", "2.31");
|
||||
|
||||
Assert.StartsWith(expectedPrefix, purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDistroPackage_IncludesArchitecture()
|
||||
{
|
||||
var purl = _builder.FromDistroPackage("deb", "debian", "libc6", "2.31", "amd64");
|
||||
|
||||
Assert.Equal("pkg:deb/debian/libc6@2.31?arch=amd64", purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDistroPackage_ThrowsForNullDistro()
|
||||
{
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
_builder.FromDistroPackage(null!, "debian", "libc6", "2.31"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDistroPackage_ThrowsForNullPackageName()
|
||||
{
|
||||
Assert.ThrowsAny<ArgumentException>(() =>
|
||||
_builder.FromDistroPackage("deb", "debian", null!, "2.31"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="NativeComponentEmitter"/>.
|
||||
/// Sprint: SPRINT_3500_0012_0001
|
||||
/// Task: BSE-008
|
||||
/// </summary>
|
||||
public sealed class NativeComponentEmitterTests
|
||||
{
|
||||
#region EmitAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_UsesIndexMatch_WhenFound()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
index.AddEntry("gnu-build-id:abc123", new BuildIdLookupResult(
|
||||
BuildId: "gnu-build-id:abc123",
|
||||
Purl: "pkg:deb/debian/libc6@2.31",
|
||||
Version: "2.31",
|
||||
SourceDistro: "debian",
|
||||
Confidence: BuildIdConfidence.Exact,
|
||||
IndexedAt: DateTimeOffset.UtcNow));
|
||||
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libc.so.6",
|
||||
BuildId = "gnu-build-id:abc123"
|
||||
};
|
||||
|
||||
var result = await emitter.EmitAsync(metadata);
|
||||
|
||||
Assert.True(result.IndexMatch);
|
||||
Assert.Equal("pkg:deb/debian/libc6@2.31", result.Purl);
|
||||
Assert.Equal("2.31", result.Version);
|
||||
Assert.NotNull(result.LookupResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_FallsBackToGenericPurl_WhenNotFound()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libcustom.so",
|
||||
BuildId = "gnu-build-id:notfound"
|
||||
};
|
||||
|
||||
var result = await emitter.EmitAsync(metadata);
|
||||
|
||||
Assert.False(result.IndexMatch);
|
||||
Assert.StartsWith("pkg:generic/libcustom.so@unknown", result.Purl);
|
||||
Assert.Null(result.LookupResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_ExtractsFilename()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/very/deep/path/to/libfoo.so.1.2.3"
|
||||
};
|
||||
|
||||
var result = await emitter.EmitAsync(metadata);
|
||||
|
||||
Assert.Equal("libfoo.so.1.2.3", result.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitAsync_UsesProductVersion_WhenNotInIndex()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
|
||||
var metadata = new NativeBinaryMetadata
|
||||
{
|
||||
Format = "pe",
|
||||
FilePath = "C:\\Windows\\System32\\kernel32.dll",
|
||||
ProductVersion = "10.0.19041.1"
|
||||
};
|
||||
|
||||
var result = await emitter.EmitAsync(metadata);
|
||||
|
||||
Assert.Equal("10.0.19041.1", result.Version);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EmitBatchAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EmitBatchAsync_ProcessesMultipleBinaries()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
index.AddEntry("gnu-build-id:aaa", new BuildIdLookupResult(
|
||||
"gnu-build-id:aaa", "pkg:deb/debian/liba@1.0", "1.0", "debian", BuildIdConfidence.Exact, DateTimeOffset.UtcNow));
|
||||
index.AddEntry("gnu-build-id:bbb", new BuildIdLookupResult(
|
||||
"gnu-build-id:bbb", "pkg:deb/debian/libb@2.0", "2.0", "debian", BuildIdConfidence.Exact, DateTimeOffset.UtcNow));
|
||||
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
|
||||
var metadataList = new[]
|
||||
{
|
||||
new NativeBinaryMetadata { Format = "elf", FilePath = "/lib/liba.so", BuildId = "gnu-build-id:aaa" },
|
||||
new NativeBinaryMetadata { Format = "elf", FilePath = "/lib/libb.so", BuildId = "gnu-build-id:bbb" },
|
||||
new NativeBinaryMetadata { Format = "elf", FilePath = "/lib/libc.so", BuildId = "gnu-build-id:ccc" }
|
||||
};
|
||||
|
||||
var results = await emitter.EmitBatchAsync(metadataList);
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
Assert.Equal(2, results.Count(r => r.IndexMatch));
|
||||
Assert.Equal(1, results.Count(r => !r.IndexMatch));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitBatchAsync_ReturnsEmptyForEmptyInput()
|
||||
{
|
||||
var index = new FakeBuildIdIndex();
|
||||
var emitter = new NativeComponentEmitter(index, NullLogger<NativeComponentEmitter>.Instance);
|
||||
|
||||
var results = await emitter.EmitBatchAsync(Array.Empty<NativeBinaryMetadata>());
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helpers
|
||||
|
||||
private sealed class FakeBuildIdIndex : IBuildIdIndex
|
||||
{
|
||||
private readonly Dictionary<string, BuildIdLookupResult> _entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public int Count => _entries.Count;
|
||||
public bool IsLoaded => true;
|
||||
|
||||
public void AddEntry(string buildId, BuildIdLookupResult result)
|
||||
{
|
||||
_entries[buildId] = result;
|
||||
}
|
||||
|
||||
public Task<BuildIdLookupResult?> LookupAsync(string buildId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_entries.TryGetValue(buildId, out var result);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<BuildIdLookupResult>> BatchLookupAsync(
|
||||
IEnumerable<string> buildIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = buildIds
|
||||
.Where(id => _entries.ContainsKey(id))
|
||||
.Select(id => _entries[id])
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<BuildIdLookupResult>>(results);
|
||||
}
|
||||
|
||||
public Task LoadAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user