();
var evidence = new[]
{
new Evidence("Periode", $"{samples.Count} min"),
new Evidence("SQL Server gns./maks.", $"{Format.Number(sql)} % / {samples.Max(s => s.SqlServerPercent)} %"),
- new Evidence("Andre processer gns.", cpu.OtherProcessesMeasurable ? $"{Format.Number(other)} %" : "kan ikke måles på Linux"),
+ new Evidence("Andre processer gns.", otherMeasurable ? $"{Format.Number(other)} %" : "kan ikke måles her"),
};
if (sql >= 75)
diff --git a/src/SQLInsight.Web/Components/Pages/Home.razor b/src/SQLInsight.Web/Components/Pages/Home.razor
index a2d464d..3a1d18a 100644
--- a/src/SQLInsight.Web/Components/Pages/Home.razor
+++ b/src/SQLInsight.Web/Components/Pages/Home.razor
@@ -44,7 +44,7 @@ else
}
| @i.Name @i.ServerName |
- @(i.ProductVersion is null ? "–" : Display.Release(i.ProductVersion)) @i.ProductVersion @i.ProductLevel @i.ProductUpdateLevel |
+ @(i.ProductVersion is null ? "–" : Display.Release(i.ProductVersion, i.EngineEdition)) @i.ProductVersion @i.ProductLevel @i.ProductUpdateLevel |
@if (i.Critical > 0) { }
@if (i.High > 0) { }
diff --git a/src/SQLInsight.Web/Components/Pages/Instance.razor b/src/SQLInsight.Web/Components/Pages/Instance.razor
index 644e84b..c150f26 100644
--- a/src/SQLInsight.Web/Components/Pages/Instance.razor
+++ b/src/SQLInsight.Web/Components/Pages/Instance.razor
@@ -15,7 +15,7 @@
Instanser / @_instance.Name
@_instance.Name
- @_instance.ServerName · @Display.Release(_instance.ProductVersion) (@_instance.ProductVersion @_instance.ProductLevel @_instance.ProductUpdateLevel) · @_instance.Edition
+ @_instance.ServerName · @Display.Release(_instance.ProductVersion, _instance.EngineEdition) (@_instance.ProductVersion @_instance.ProductLevel @_instance.ProductUpdateLevel) · @_instance.Edition
· sidst set @Display.Ago(_instance.LastSeenAt)
diff --git a/src/SQLInsight.Web/Display.cs b/src/SQLInsight.Web/Display.cs
index d2395e8..bc394be 100644
--- a/src/SQLInsight.Web/Display.cs
+++ b/src/SQLInsight.Web/Display.cs
@@ -55,6 +55,7 @@ public sealed class Display(IOptions options)
_ => "sev-ok",
};
- public static string Release(string? productVersion) =>
- SqlServerVersion.TryParse(productVersion, out var version) ? version.Release.DisplayName() : "Ukendt version";
+ public static string Release(string? productVersion, int? engineEdition = null) =>
+ engineEdition == (int)EngineEdition.AzureSqlManagedInstance ? SqlServerRelease.AzureSqlManagedInstance.DisplayName()
+ : SqlServerVersion.TryParse(productVersion, out var version) ? version.Release.DisplayName() : "Ukendt version";
}
diff --git a/tests/SQLInsight.Connectivity.Tests/ConnectionStringsTests.cs b/tests/SQLInsight.Connectivity.Tests/ConnectionStringsTests.cs
index bfc434b..dbcf235 100644
--- a/tests/SQLInsight.Connectivity.Tests/ConnectionStringsTests.cs
+++ b/tests/SQLInsight.Connectivity.Tests/ConnectionStringsTests.cs
@@ -40,3 +40,36 @@ public class ConnectionStringsTests
Assert.Throws(() => ConnectionStrings.ForSqlClient(target));
}
}
+
+public class EntraTests
+{
+ [Fact]
+ public void Entra_connection_string_carries_no_credentials()
+ {
+ var cs = ConnectionStrings.ForSqlClient(new ConnectionTarget { Server = "mi.abc.database.windows.net", Authentication = AuthenticationMode.EntraManagedIdentity });
+
+ Assert.DoesNotContain("Integrated Security=True", cs, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("User ID", cs, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("Password", cs, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Entra_is_not_offered_over_odbc()
+ {
+ Assert.Throws(() => ConnectionStrings.ForOdbc(new ConnectionTarget { Server = "x", Authentication = AuthenticationMode.EntraDefault }));
+ }
+
+ [Theory]
+ [InlineData(AuthenticationMode.EntraManagedIdentity, "Azure.Identity.ManagedIdentityCredential")]
+ [InlineData(AuthenticationMode.EntraDefault, "Azure.Identity.DefaultAzureCredential")]
+ public void Credential_type_follows_the_mode(AuthenticationMode mode, string type)
+ {
+ Assert.Equal(type, EntraCredentials.For(new ConnectionTarget { Server = "x", Authentication = mode }).GetType().FullName);
+ }
+
+ [Fact]
+ public void Service_principal_needs_tenant_client_and_secret()
+ {
+ Assert.Throws(() => EntraCredentials.For(new ConnectionTarget { Server = "x", Authentication = AuthenticationMode.EntraServicePrincipal, UserName = "client" }));
+ }
+}
diff --git a/tests/SQLInsight.Core.Tests/InstanceCapabilitiesTests.cs b/tests/SQLInsight.Core.Tests/InstanceCapabilitiesTests.cs
index 900289a..1f4fb12 100644
--- a/tests/SQLInsight.Core.Tests/InstanceCapabilitiesTests.cs
+++ b/tests/SQLInsight.Core.Tests/InstanceCapabilitiesTests.cs
@@ -18,7 +18,23 @@ public class InstanceCapabilitiesTests
{
var capabilities = InstanceCapabilities.From(Instance("8.00.2039"));
- Assert.Empty(capabilities.All);
+ Assert.Equal([Capability.SelfManagedInfrastructure], capabilities.All);
+ }
+
+ [Fact]
+ public void Managed_instance_is_evergreen_and_not_self_managed()
+ {
+ // Azure SQL Managed Instance always reports 12.0.2000.8.
+ var mi = Instance("12.0.2000.8") with { EngineEdition = EngineEdition.AzureSqlManagedInstance, IsHadrEnabled = true };
+
+ var capabilities = InstanceCapabilities.From(mi);
+
+ Assert.Equal(SqlServerRelease.AzureSqlManagedInstance, mi.Release);
+ Assert.True(capabilities.Has(Capability.QueryStore));
+ Assert.True(capabilities.Has(Capability.GranularServerPermissions));
+ Assert.True(capabilities.Has(Capability.QueryStatsMemoryColumns));
+ Assert.False(capabilities.Has(Capability.SelfManagedInfrastructure));
+ Assert.False(capabilities.Has(Capability.HadrEnabled)); // Azure runs the availability of MI
}
[Fact]
diff --git a/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs b/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs
new file mode 100644
index 0000000..7ac4ec2
--- /dev/null
+++ b/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs
@@ -0,0 +1,45 @@
+using SQLInsight.Core.Instances;
+using SQLInsight.Rules.Engine;
+using static SQLInsight.Rules.Tests.TestInstances;
+
+namespace SQLInsight.Rules.Tests;
+
+public class ManagedInstanceTests
+{
+ private static RuleContext ManagedInstance()
+ {
+ var info = new InstanceInfo
+ {
+ ServerName = "mi-prod.abc123.database.windows.net",
+ Version = SqlServerVersion.Parse("12.0.2000.8"),
+ Edition = "SQL Azure",
+ EngineEdition = EngineEdition.AzureSqlManagedInstance,
+ };
+ var context = new RuleContext(info, InstanceCapabilities.From(info), Now);
+ context.Data.Set(Config(("max server memory (MB)", 2147483647), ("cost threshold for parallelism", 5), ("max degree of parallelism", 0), ("xp_cmdshell", 0)));
+ context.Data.Set(Server(cpus: 8));
+ context.Data.Set(DatabaseList(Database(change: d => d with { LastFullBackup = null, LastGoodCheckDb = new DateTime(1900, 1, 1) })));
+ return context;
+ }
+
+ [Fact]
+ public void Azure_managed_areas_are_reported_as_handled_by_azure_not_as_findings()
+ {
+ var results = new RuleEngine(BuiltInRules.Create()).Evaluate(ManagedInstance());
+
+ foreach (var id in new[] { "SQLI-VER-001", "SQLI-VER-002", "SQLI-CFG-001", "SQLI-DB-007", "SQLI-DB-009", "SQLI-DB-015" })
+ {
+ var result = results.Single(r => r.RuleId == id);
+ Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
+ Assert.Equal("Håndteres af Azure på Azure SQL Managed Instance.", result.Reason);
+ }
+ }
+
+ [Fact]
+ public void Workload_rules_still_apply()
+ {
+ var results = new RuleEngine(BuiltInRules.Create()).Evaluate(ManagedInstance());
+
+ Assert.Equal(RuleOutcome.Findings, results.Single(r => r.RuleId == "SQLI-CFG-003").Outcome); // cost threshold 5
+ }
+}
diff --git a/tests/SQLInsight.Rules.Tests/PatchLevelRuleTests.cs b/tests/SQLInsight.Rules.Tests/PatchLevelRuleTests.cs
index 915d53d..5b5a6fb 100644
--- a/tests/SQLInsight.Rules.Tests/PatchLevelRuleTests.cs
+++ b/tests/SQLInsight.Rules.Tests/PatchLevelRuleTests.cs
@@ -15,7 +15,7 @@ public class PatchLevelRuleTests
{
var catalog = BuildCatalog.LoadEmbedded();
- foreach (var release in Enum.GetValues().Where(r => r != SqlServerRelease.Unknown))
+ foreach (var release in Enum.GetValues().Where(r => r is not (SqlServerRelease.Unknown or SqlServerRelease.AzureSqlManagedInstance)))
{
var latest = catalog.Latest(release);
Assert.NotNull(latest);
diff --git a/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs b/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs
new file mode 100644
index 0000000..f68ef64
--- /dev/null
+++ b/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs
@@ -0,0 +1,23 @@
+using SQLInsight.Rules.Engine;
+
+namespace SQLInsight.Rules.Tests;
+
+public class RuleCatalogTests
+{
+ [Fact]
+ public void Every_rule_has_a_unique_well_formed_id()
+ {
+ var rules = BuiltInRules.Create();
+
+ Assert.Equal(rules.Count, rules.Select(r => r.Id).Distinct(StringComparer.OrdinalIgnoreCase).Count());
+ Assert.All(rules, r => Assert.Matches("^SQLI-[A-Z]+-[0-9]{3}$", r.Id));
+ Assert.All(rules, r => Assert.False(string.IsNullOrWhiteSpace(r.Title)));
+ }
+
+ /// Keeps the rule count in the README honest.
+ [Fact]
+ public void Rule_count()
+ {
+ Assert.Equal(61, BuiltInRules.Create().Count);
+ }
+}
diff --git a/tests/SQLInsight.Rules.Tests/SupportLifecycleRuleTests.cs b/tests/SQLInsight.Rules.Tests/SupportLifecycleRuleTests.cs
index 3ed9541..fa7e2d3 100644
--- a/tests/SQLInsight.Rules.Tests/SupportLifecycleRuleTests.cs
+++ b/tests/SQLInsight.Rules.Tests/SupportLifecycleRuleTests.cs
@@ -85,9 +85,10 @@ public class SupportLifecycleRuleTests
}
[Fact]
- public void Runs_on_sql2000_because_it_needs_no_capabilities()
+ public void Runs_on_sql2000_but_not_on_managed_instance()
{
- Assert.Empty(Rule.RequiredCapabilities);
+ Assert.Equal([Capability.SelfManagedInfrastructure], Rule.RequiredCapabilities);
+ Assert.Equal(RuleOutcome.Findings, new RuleEngine([Rule]).Evaluate(TestInstances.Context("8.00.2039")).Single().Outcome);
}
[Fact]
@@ -95,7 +96,7 @@ public class SupportLifecycleRuleTests
{
var catalog = SupportLifecycleCatalog.LoadEmbedded();
- foreach (var release in Enum.GetValues().Where(r => r != SqlServerRelease.Unknown))
+ foreach (var release in Enum.GetValues().Where(r => r is not (SqlServerRelease.Unknown or SqlServerRelease.AzureSqlManagedInstance)))
{
var lifecycle = catalog.Find(release);
Assert.NotNull(lifecycle);
|