Add unit tests and implementations for MongoDB index models and OpenAPI metadata
- Implemented `MongoIndexModelTests` to verify index models for various stores. - Created `OpenApiMetadataFactory` with methods to generate OpenAPI metadata. - Added tests for `OpenApiMetadataFactory` to ensure expected defaults and URL overrides. - Introduced `ObserverSurfaceSecrets` and `WebhookSurfaceSecrets` for managing secrets. - Developed `RuntimeSurfaceFsClient` and `WebhookSurfaceFsClient` for manifest retrieval. - Added dependency injection tests for `SurfaceEnvironmentRegistration` in both Observer and Webhook contexts. - Implemented tests for secret resolution in `ObserverSurfaceSecretsTests` and `WebhookSurfaceSecretsTests`. - Created `EnsureLinkNotMergeCollectionsMigrationTests` to validate MongoDB migration logic. - Added project files for MongoDB tests and NuGet package mirroring.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves publish artifacts (deps/runtimeconfig) into deterministic entrypoint identities.
|
||||
/// </summary>
|
||||
public static class DotNetEntrypointResolver
|
||||
{
|
||||
private static readonly EnumerationOptions Enumeration = new()
|
||||
{
|
||||
RecurseSubdirectories = true,
|
||||
IgnoreInaccessible = true,
|
||||
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint
|
||||
};
|
||||
|
||||
public static ValueTask<IReadOnlyList<DotNetEntrypoint>> ResolveAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
var depsFiles = Directory
|
||||
.EnumerateFiles(context.RootPath, "*.deps.json", Enumeration)
|
||||
.OrderBy(static path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (depsFiles.Length == 0)
|
||||
{
|
||||
return ValueTask.FromResult<IReadOnlyList<DotNetEntrypoint>>(Array.Empty<DotNetEntrypoint>());
|
||||
}
|
||||
|
||||
var results = new List<DotNetEntrypoint>(depsFiles.Length);
|
||||
|
||||
foreach (var depsPath in depsFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var relativeDepsPath = NormalizeRelative(context.GetRelativePath(depsPath));
|
||||
var depsFile = DotNetDepsFile.Load(depsPath, relativeDepsPath, cancellationToken);
|
||||
if (depsFile is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DotNetRuntimeConfig? runtimeConfig = null;
|
||||
var runtimeConfigPath = Path.ChangeExtension(depsPath, ".runtimeconfig.json");
|
||||
string? relativeRuntimeConfig = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(runtimeConfigPath) && File.Exists(runtimeConfigPath))
|
||||
{
|
||||
relativeRuntimeConfig = NormalizeRelative(context.GetRelativePath(runtimeConfigPath));
|
||||
runtimeConfig = DotNetRuntimeConfig.Load(runtimeConfigPath, relativeRuntimeConfig, cancellationToken);
|
||||
}
|
||||
|
||||
var tfms = CollectTargetFrameworks(depsFile, runtimeConfig);
|
||||
var rids = CollectRuntimeIdentifiers(depsFile, runtimeConfig);
|
||||
var publishKind = DeterminePublishKind(depsFile);
|
||||
|
||||
var name = GetEntrypointName(depsPath);
|
||||
var id = BuildDeterministicId(name, tfms, rids, publishKind);
|
||||
|
||||
results.Add(new DotNetEntrypoint(
|
||||
Id: id,
|
||||
Name: name,
|
||||
TargetFrameworks: tfms,
|
||||
RuntimeIdentifiers: rids,
|
||||
RelativeDepsPath: relativeDepsPath,
|
||||
RelativeRuntimeConfigPath: relativeRuntimeConfig,
|
||||
PublishKind: publishKind));
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<IReadOnlyList<DotNetEntrypoint>>(results);
|
||||
}
|
||||
|
||||
private static string GetEntrypointName(string depsPath)
|
||||
{
|
||||
// Strip .json then any trailing .deps suffix to yield a logical entrypoint name.
|
||||
var stem = Path.GetFileNameWithoutExtension(depsPath); // removes .json
|
||||
|
||||
if (stem.EndsWith(".deps", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stem = stem[..^".deps".Length];
|
||||
}
|
||||
|
||||
return stem;
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<string> CollectTargetFrameworks(DotNetDepsFile depsFile, DotNetRuntimeConfig? runtimeConfig)
|
||||
{
|
||||
var tfms = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var library in depsFile.Libraries.Values)
|
||||
{
|
||||
foreach (var tfm in library.TargetFrameworks)
|
||||
{
|
||||
tfms.Add(tfm);
|
||||
}
|
||||
}
|
||||
|
||||
if (runtimeConfig is not null)
|
||||
{
|
||||
foreach (var tfm in runtimeConfig.Tfms)
|
||||
{
|
||||
tfms.Add(tfm);
|
||||
}
|
||||
|
||||
foreach (var framework in runtimeConfig.Frameworks)
|
||||
{
|
||||
tfms.Add(framework);
|
||||
}
|
||||
}
|
||||
|
||||
return tfms;
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<string> CollectRuntimeIdentifiers(DotNetDepsFile depsFile, DotNetRuntimeConfig? runtimeConfig)
|
||||
{
|
||||
var rids = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var library in depsFile.Libraries.Values)
|
||||
{
|
||||
foreach (var rid in library.RuntimeIdentifiers)
|
||||
{
|
||||
rids.Add(rid);
|
||||
}
|
||||
}
|
||||
|
||||
if (runtimeConfig is not null)
|
||||
{
|
||||
foreach (var entry in runtimeConfig.RuntimeGraph)
|
||||
{
|
||||
rids.Add(entry.Rid);
|
||||
|
||||
foreach (var fallback in entry.Fallbacks)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(fallback))
|
||||
{
|
||||
rids.Add(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rids;
|
||||
}
|
||||
|
||||
private static DotNetPublishKind DeterminePublishKind(DotNetDepsFile depsFile)
|
||||
{
|
||||
foreach (var library in depsFile.Libraries.Values)
|
||||
{
|
||||
if (library.Id.StartsWith("Microsoft.NETCore.App.Runtime.", StringComparison.OrdinalIgnoreCase) ||
|
||||
library.Id.StartsWith("Microsoft.WindowsDesktop.App.Runtime.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return DotNetPublishKind.SelfContained;
|
||||
}
|
||||
}
|
||||
|
||||
return DotNetPublishKind.FrameworkDependent;
|
||||
}
|
||||
|
||||
private static string BuildDeterministicId(
|
||||
string name,
|
||||
IReadOnlyCollection<string> tfms,
|
||||
IReadOnlyCollection<string> rids,
|
||||
DotNetPublishKind publishKind)
|
||||
{
|
||||
var tfmPart = tfms.Count == 0 ? "unknown" : string.Join('+', tfms.OrderBy(t => t, StringComparer.OrdinalIgnoreCase));
|
||||
var ridPart = rids.Count == 0 ? "none" : string.Join('+', rids.OrderBy(r => r, StringComparer.OrdinalIgnoreCase));
|
||||
var publishPart = publishKind.ToString().ToLowerInvariant();
|
||||
return $"{name}:{tfmPart}:{ridPart}:{publishPart}";
|
||||
}
|
||||
|
||||
private static string NormalizeRelative(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || path == ".")
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
var normalized = path.Replace('\\', '/');
|
||||
return string.IsNullOrWhiteSpace(normalized) ? "." : normalized;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record DotNetEntrypoint(
|
||||
string Id,
|
||||
string Name,
|
||||
IReadOnlyCollection<string> TargetFrameworks,
|
||||
IReadOnlyCollection<string> RuntimeIdentifiers,
|
||||
string RelativeDepsPath,
|
||||
string? RelativeRuntimeConfigPath,
|
||||
DotNetPublishKind PublishKind);
|
||||
|
||||
public enum DotNetPublishKind
|
||||
{
|
||||
Unknown = 0,
|
||||
FrameworkDependent = 1,
|
||||
SelfContained = 2
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
public sealed class AnalysisSnapshot
|
||||
{
|
||||
public IReadOnlyList<LanguageEntrypointSnapshot> Entrypoints { get; set; } = Array.Empty<LanguageEntrypointSnapshot>();
|
||||
public IReadOnlyList<LanguageComponentSnapshot> Components { get; set; } = Array.Empty<LanguageComponentSnapshot>();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
internal static class LanguageComponentEvidenceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a stable key for evidence items to support deterministic dictionaries.
|
||||
/// </summary>
|
||||
public static string ToKey(this LanguageComponentEvidence evidence)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
return $"{evidence.Kind}:{evidence.Source}:{evidence.Locator}".ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
public sealed class LanguageEntrypointRecord
|
||||
{
|
||||
private readonly SortedDictionary<string, string?> _metadata;
|
||||
private readonly SortedDictionary<string, LanguageComponentEvidence> _evidence;
|
||||
|
||||
public LanguageEntrypointRecord(
|
||||
string id,
|
||||
string name,
|
||||
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
|
||||
IEnumerable<LanguageComponentEvidence>? evidence = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentException("Entrypoint id is required", nameof(id));
|
||||
}
|
||||
|
||||
Id = id.Trim();
|
||||
Name = string.IsNullOrWhiteSpace(name) ? Id : name.Trim();
|
||||
|
||||
_metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal);
|
||||
foreach (var pair in metadata ?? Array.Empty<KeyValuePair<string, string?>>())
|
||||
{
|
||||
_metadata[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
_evidence = new SortedDictionary<string, LanguageComponentEvidence>(StringComparer.Ordinal);
|
||||
foreach (var item in evidence ?? Array.Empty<LanguageComponentEvidence>())
|
||||
{
|
||||
var key = item.ToKey();
|
||||
_evidence[key] = item;
|
||||
}
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string?> Metadata => _metadata;
|
||||
|
||||
public IReadOnlyCollection<LanguageComponentEvidence> Evidence => _evidence.Values;
|
||||
|
||||
internal LanguageEntrypointSnapshot ToSnapshot()
|
||||
=> new()
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name,
|
||||
Metadata = _metadata.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal),
|
||||
Evidence = _evidence.Values
|
||||
.Select(static item => new LanguageComponentEvidenceSnapshot
|
||||
{
|
||||
Kind = item.Kind,
|
||||
Source = item.Source,
|
||||
Locator = item.Locator,
|
||||
Value = item.Value,
|
||||
Sha256 = item.Sha256
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
internal static LanguageEntrypointRecord FromSnapshot(LanguageEntrypointSnapshot snapshot)
|
||||
{
|
||||
if (snapshot is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(snapshot));
|
||||
}
|
||||
|
||||
var evidence = snapshot.Evidence?
|
||||
.Select(static e => new LanguageComponentEvidence(e.Kind, e.Source, e.Locator, e.Value, e.Sha256))
|
||||
.ToArray();
|
||||
|
||||
return new LanguageEntrypointRecord(
|
||||
snapshot.Id ?? string.Empty,
|
||||
snapshot.Name ?? snapshot.Id ?? string.Empty,
|
||||
snapshot.Metadata,
|
||||
evidence);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LanguageEntrypointSnapshot
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, string?> Metadata { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
[JsonPropertyName("evidence")]
|
||||
public List<LanguageComponentEvidenceSnapshot> Evidence { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# EntryTrace Tasks
|
||||
|
||||
| Task ID | Status | Date | Summary |
|
||||
| --- | --- | --- | --- |
|
||||
| SCANNER-ENG-0008 | DONE | 2025-11-16 | Documented quarterly EntryTrace heuristic cadence and workflow; attached to Sprint 0138 Execution Log. |
|
||||
Reference in New Issue
Block a user