using System.Text.Json;
using AutopilotMonitor.Functions.Services;
namespace AutopilotMonitor.Functions.Tests;
///
/// Unit tests for the dot-path resolver inside .
/// Exercises both nested Dictionary<string,object> and
/// payload shapes — both occur in production depending on whether the event came in via
/// the in-process emitter (typed sidecar) or was rehydrated from Table Storage JSON.
///
public class RuleEngineDataPathResolverTests
{
[Fact]
public void TryFlatLookup_LiteralKeyWithDot_StillResolves()
{
// Pre-existing v1 events used keys like "scan_summary.critical_cves" with a literal
// dot. The flat lookup must keep working so legacy rules don't break.
var data = new Dictionary
{
["scan_summary.critical_cves"] = 7,
};
var ok = RuleEngine.TryFlatLookup(data, "scan_summary.critical_cves", out var value);
Assert.True(ok);
Assert.Equal("7", value);
}
[Fact]
public void TryFlatLookup_CaseInsensitive()
{
var data = new Dictionary { ["FailedSubcategories"] = "Certificates,VPN" };
var ok = RuleEngine.TryFlatLookup(data, "failedsubcategories", out var value);
Assert.True(ok);
Assert.Equal("Certificates,VPN", value);
}
[Fact]
public void ResolveDotPath_NestedDictionary_ReturnsLeafScalar()
{
var data = new Dictionary
{
["scan_summary"] = new Dictionary
{
["critical_cves"] = 3,
["overall_risk"] = "high",
},
};
Assert.Equal("3", RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
Assert.Equal("high", RuleEngine.ResolveDotPath(data, "scan_summary.overall_risk"));
}
[Fact]
public void ResolveDotPath_NestedJsonElement_ReturnsLeafScalar()
{
// Events read back from Table Storage hit the resolver as JsonElement subtrees.
var json = """{"scan_summary":{"critical_cves":5,"kev_matches":2,"overall_risk":"critical"}}""";
using var doc = JsonDocument.Parse(json);
var data = new Dictionary
{
["scan_summary"] = doc.RootElement.GetProperty("scan_summary").Clone(),
};
Assert.Equal("5", RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
Assert.Equal("2", RuleEngine.ResolveDotPath(data, "scan_summary.kev_matches"));
Assert.Equal("critical", RuleEngine.ResolveDotPath(data, "scan_summary.overall_risk"));
}
[Fact]
public void ResolveDotPath_CaseInsensitiveAtEachLevel()
{
var data = new Dictionary
{
["Scan_Summary"] = new Dictionary
{
["Critical_CVEs"] = 4,
},
};
Assert.Equal("4", RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
}
[Fact]
public void ResolveDotPath_MissingPath_ReturnsNull()
{
var data = new Dictionary
{
["scan_summary"] = new Dictionary { ["critical_cves"] = 1 },
};
Assert.Null(RuleEngine.ResolveDotPath(data, "scan_summary.missing"));
Assert.Null(RuleEngine.ResolveDotPath(data, "missing.critical_cves"));
}
[Fact]
public void ResolveDotPath_DescendIntoScalar_ReturnsNull()
{
// Path traverses past a scalar — must not throw, must return null so the rule
// misses cleanly instead of stringifying an unrelated value.
var data = new Dictionary
{
["scan_summary"] = "not-an-object",
};
Assert.Null(RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
}
[Fact]
public void TryFlatLookup_ListValue_FlattensToJoinedString()
{
// Regression: app_tracking_summary.pendingNames is a List