Files
sql-utilities/tests/Strata.SqlTools.LinqToSql.Tests/QueryValidatorTests.cs
T
Thom LambandClaude Opus 4.7 f55d8130e6
SonarQube Analysis / sonarqube (pull_request) Successful in 3m41s
chore(sonar): clear residual test-file smells (NUnit2046, NUnit2045, CS8604)
- **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>
2026-05-27 15:29:20 -05:00

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
}