SonarQube Analysis / sonarqube (pull_request) Successful in 3m41s
- **NUnit2046 (15)**: extend the prior regex sweep to also catch the
`Is.GreaterThan(n)` / `Is.GreaterThanOrEqualTo(n)` variants on
`.Count` and `.Length` — the previous pass only handled `Is.EqualTo`.
Affects PG/Snowflake `QueryBreakdownTests`, PG `StatementReaderTests`,
`LinqToSql` `QueryComparatorTests` / `QueryValidatorTests`,
`Markdown.Tests` `ExpressionGeneratorTests` /
`QueryBreakdownGeneratorTests`, and `JsonTokenReaderTests`.
- **CS8604 (8)**: `merged[someKey]` access inside `Assert.Multiple(() =>
{ ... })` lambdas where `someKey` is `string?` from `FirstOrDefault`.
The preceding `Assert.That(someKey, Is.Not.Null)` does not propagate
null-narrowing into the lambda scope, so add `!` to the dictionary
index. (Tests fail loud with a meaningful message if the key really
is null, so this is safe.)
- **NUnit2045 (1)**: wrap a four-assert block in
`JsonTokenReaderTests.cs:65-68` in `Assert.Multiple`. Mirrors the
surrounding two `Assert.Multiple` groups in that method.
All 1180 tests stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
600 lines
16 KiB
C#
600 lines
16 KiB
C#
using NUnit.Framework;
|
|
using Strata.SqlTools.Builders.LinqToSql;
|
|
using Strata.SqlTools.Validators.LinqToSql;
|
|
|
|
namespace Strata.SqlTools.LinqToSql.Tests;
|
|
|
|
[TestFixture]
|
|
public class QueryValidatorTests
|
|
{
|
|
#region Basic Validation Tests
|
|
|
|
[Test]
|
|
public void Validate_WithValidQuery_HasNoErrors()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectColumns("Id", "Name")
|
|
.FromTable("Users")
|
|
.Where("IsActive = true")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
Assert.That(validator.HasErrors, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_WithNull_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var validator = QueryValidator.Create();
|
|
|
|
// Act & Assert
|
|
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
|
|
Assert.Throws<ArgumentNullException>(() => validator.Validate(null));
|
|
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_CanBeCalledMultipleTimes()
|
|
{
|
|
// Arrange
|
|
var validator = QueryValidator.Create();
|
|
var breakdown1 = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
var breakdown2 = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Orders")
|
|
.Build();
|
|
|
|
// Act
|
|
validator.Validate(breakdown1);
|
|
var count1 = validator.Issues.Count;
|
|
validator.Clear();
|
|
validator.Validate(breakdown2);
|
|
var count2 = validator.Issues.Count;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(count1, Is.GreaterThan(0));
|
|
Assert.That(count2, Is.GreaterThan(0));
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SELECT Clause Validation Tests
|
|
|
|
[Test]
|
|
public void Validate_SelectStarQuery_HasSelectAllColumnsWarning()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "SELECT_ALL_COLUMNS");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Warning));
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_SelectManyColumns_HasSelectTooManyWarning()
|
|
{
|
|
// Arrange
|
|
var columns = Enumerable.Range(1, 25).Select(i => $"Column{i}").ToArray();
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectColumns(columns)
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "SELECT_TOO_MANY");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Warning));
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_MissingSelectClause_HasSelectMissingError()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
breakdown.SelectClause.Clause = string.Empty;
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
Assert.That(validator.HasErrors, Is.True);
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "SELECT_MISSING");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Error));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region FROM Clause Validation Tests
|
|
|
|
[Test]
|
|
public void Validate_MissingFromClause_HasFromMissingError()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
breakdown.FromClause.Clause = string.Empty;
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
Assert.That(validator.HasErrors, Is.True);
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "FROM_MISSING");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Error));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GROUP BY + HAVING Validation Tests
|
|
|
|
[Test]
|
|
public void Validate_HavingWithoutGroupBy_HasHavingWithoutGroupByError()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Having("COUNT(*) > 5")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
Assert.That(validator.HasErrors, Is.True);
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "HAVING_WITHOUT_GROUPBY");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Error));
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_GroupByWithHaving_HasNoGroupByError()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectColumns("Department", "COUNT(*)")
|
|
.FromTable("Employees")
|
|
.GroupBy("Department")
|
|
.Having("COUNT(*) > 5")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "HAVING_WITHOUT_GROUPBY");
|
|
Assert.That(issue, Is.Null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region WHERE Clause Validation Tests
|
|
|
|
[Test]
|
|
public void Validate_NoWhereClause_HasNoWhereClauseInfo()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "NO_WHERE_CLAUSE");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Warning));
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_WithWhereClause_NoWhereWarning()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Where("IsActive = true")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "NO_WHERE_CLAUSE");
|
|
Assert.That(issue, Is.Null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ORDER BY Validation Tests
|
|
|
|
[Test]
|
|
public void Validate_NoOrderByOrGroupBy_HasNoOrderByInfo()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "NO_ORDER_BY");
|
|
Assert.That(issue, Is.Not.Null);
|
|
Assert.That(issue!.Severity, Is.EqualTo(ValidationSeverity.Info));
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_WithOrderBy_NoOrderByInfo()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.OrderByAscending("Name")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "NO_ORDER_BY");
|
|
Assert.That(issue, Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_WithGroupBy_NoOrderByInfo()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectColumns("Department", "COUNT(*)")
|
|
.FromTable("Employees")
|
|
.GroupBy("Department")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
var issue = validator.Issues.FirstOrDefault(i => i.Code == "NO_ORDER_BY");
|
|
Assert.That(issue, Is.Null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Issue Filtering Tests
|
|
|
|
[Test]
|
|
public void GetIssuesBySeverity_FiltersCorrectly()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
var warnings = validator.GetIssuesBySeverity(ValidationSeverity.Warning);
|
|
var infos = validator.GetIssuesBySeverity(ValidationSeverity.Info);
|
|
var errors = validator.GetIssuesBySeverity(ValidationSeverity.Error);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(warnings, Is.Not.Empty);
|
|
Assert.That(infos, Is.Not.Empty);
|
|
Assert.That(errors, Is.Empty);
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Custom Issue Tests
|
|
|
|
[Test]
|
|
public void AddIssue_AddsCustomIssue()
|
|
{
|
|
// Arrange
|
|
var validator = QueryValidator.Create();
|
|
|
|
// Act
|
|
validator.AddIssue(
|
|
ValidationSeverity.Error,
|
|
"CUSTOM_001",
|
|
"Custom validation error",
|
|
"This is a custom issue");
|
|
|
|
// Assert
|
|
Assert.That(validator.Issues, Has.Count.EqualTo(1));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(validator.Issues[0].Code, Is.EqualTo("CUSTOM_001"));
|
|
Assert.That(validator.HasErrors, Is.True);
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void AddIssue_SupportsMethodChaining()
|
|
{
|
|
// Arrange
|
|
var validator = QueryValidator.Create();
|
|
|
|
// Act
|
|
var result = validator
|
|
.AddIssue(ValidationSeverity.Error, "ERR_001", "Error 1")
|
|
.AddIssue(ValidationSeverity.Warning, "WARN_001", "Warning 1")
|
|
.AddIssue(ValidationSeverity.Info, "INFO_001", "Info 1");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(result, Is.SameAs(validator));
|
|
Assert.That(validator.Issues, Has.Count.EqualTo(3));
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Clear Tests
|
|
|
|
[Test]
|
|
public void Clear_RemovesAllIssues()
|
|
{
|
|
// Arrange
|
|
var validator = QueryValidator.Create();
|
|
validator.AddIssue(ValidationSeverity.Error, "ERR", "Error");
|
|
validator.AddIssue(ValidationSeverity.Warning, "WARN", "Warning");
|
|
|
|
// Act
|
|
validator.Clear();
|
|
|
|
// Assert
|
|
Assert.That(validator.Issues, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void Clear_ReturnsSelf()
|
|
{
|
|
// Arrange
|
|
var validator = QueryValidator.Create();
|
|
|
|
// Act
|
|
var result = validator.Clear();
|
|
|
|
// Assert
|
|
Assert.That(result, Is.SameAs(validator));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Report Generation Tests
|
|
|
|
[Test]
|
|
public void GetReport_WithNoIssues_ShowsSuccess()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectColumns("Id", "Name")
|
|
.FromTable("Users")
|
|
.Where("IsActive = true")
|
|
.OrderByAscending("Name")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
var report = validator.GetReport();
|
|
|
|
// Assert
|
|
Assert.That(report, Contains.Substring("No validation issues"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetReport_WithIssues_ListsEachIssue()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
var report = validator.GetReport();
|
|
|
|
// Assert
|
|
Assert.That(report, Contains.Substring("WARNINGS").IgnoreCase);
|
|
Assert.That(report, Contains.Substring("Validation Report").IgnoreCase);
|
|
}
|
|
|
|
[Test]
|
|
public void GetReport_IncludesIssueDetails()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
var report = validator.GetReport();
|
|
|
|
// Assert
|
|
Assert.That(report, Contains.Substring("SELECT_ALL_COLUMNS"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Complex Anti-Pattern Tests
|
|
|
|
[Test]
|
|
public void Validate_ComplexProblematicQuery_FindsMultipleIssues()
|
|
{
|
|
// Arrange - Query with SELECT *, no WHERE, no ORDER BY
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("LargeTable")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(validator.Issues, Has.Count.GreaterThan(1));
|
|
Assert.That(validator.HasWarnings, Is.True);
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void Validate_WellFormedAggregationQuery_HasMinimalIssues()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectColumns("Category", "COUNT(*) as ProductCount", "SUM(Price) as TotalValue")
|
|
.FromTable("Products")
|
|
.Where("IsActive = true")
|
|
.GroupBy("Category")
|
|
.OrderByDescending("TotalValue")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
Assert.That(validator.HasErrors, Is.False);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Static Factory Tests
|
|
|
|
[Test]
|
|
public void Create_ReturnsNewValidator()
|
|
{
|
|
// Act
|
|
var validator = QueryValidator.Create();
|
|
|
|
// Assert
|
|
Assert.That(validator, Is.Not.Null);
|
|
Assert.That(validator, Is.TypeOf<QueryValidator>());
|
|
}
|
|
|
|
[Test]
|
|
public void ValidateQuery_ReturnsValidatorWithResults()
|
|
{
|
|
// Arrange
|
|
var breakdown = LinqQueryBreakdownBuilder
|
|
.Create()
|
|
.SelectAll()
|
|
.FromTable("Users")
|
|
.Build();
|
|
|
|
// Act
|
|
var validator = QueryValidator.ValidateQuery(breakdown);
|
|
|
|
// Assert
|
|
Assert.That(validator, Is.Not.Null);
|
|
Assert.That(validator.Issues, Is.Not.Empty);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region String Representation Tests
|
|
|
|
[Test]
|
|
public void QueryValidationIssue_ToString_FormatsCorrectly()
|
|
{
|
|
// Arrange
|
|
var issue = new QueryValidationIssue(
|
|
ValidationSeverity.Error,
|
|
"TEST_001",
|
|
"Test error message",
|
|
"Additional details");
|
|
|
|
// Act
|
|
var str = issue.ToString();
|
|
|
|
// Assert
|
|
Assert.That(str, Contains.Substring("Error"));
|
|
Assert.That(str, Contains.Substring("TEST_001"));
|
|
Assert.That(str, Contains.Substring("Test error message"));
|
|
Assert.That(str, Contains.Substring("Additional details"));
|
|
}
|
|
|
|
[Test]
|
|
public void QueryValidationIssue_ToStringWithoutDetails_FormatsCorrectly()
|
|
{
|
|
// Arrange
|
|
var issue = new QueryValidationIssue(
|
|
ValidationSeverity.Warning,
|
|
"WARN_001",
|
|
"Warning message");
|
|
|
|
// Act
|
|
var str = issue.ToString();
|
|
|
|
// Assert
|
|
Assert.That(str, Contains.Substring("Warning"));
|
|
Assert.That(str, Contains.Substring("WARN_001"));
|
|
Assert.That(str, Contains.Substring("Warning message"));
|
|
}
|
|
|
|
#endregion
|
|
}
|