Driven by `dotnet format analyzers --diagnostics NUnit2045`. The fixer
groups consecutive independent `Assert.That(...)` calls into
`Assert.Multiple(() => { ... })`, so a failing assertion no longer
short-circuits the block — every failure inside the group is reported,
which gives much better diagnostics on multi-property tests.
Audit confirmed no Assert.Throws / Assert.Fail / Assert.Catch / Assert.Pass
/ Assert.DoesNotThrow got pulled inside a Multiple block (those need to
short-circuit). All 1180 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
478 lines
14 KiB
C#
478 lines
14 KiB
C#
using NUnit.Framework;
|
|
using Strata.SqlTools.Breakdowns.SqlServer;
|
|
using Strata.SqlTools.SqlBreakdown.Classes;
|
|
using Strata.SqlTools.SqlBreakdown.Exceptions;
|
|
using Strata.SqlTools.SqlServer.Exceptions;
|
|
|
|
namespace Strata.SqlTools.SqlBreakdown.Tests.Exceptions;
|
|
|
|
[TestFixture]
|
|
public class ExceptionHandlingTests
|
|
{
|
|
#region SqlParseException Tests
|
|
|
|
[Test]
|
|
public void SqlParseException_StoresProperties()
|
|
{
|
|
// Arrange
|
|
var message = "Unexpected token";
|
|
var sql = "SELECT * FRM users";
|
|
var position = 10;
|
|
|
|
// Act
|
|
var exception = new SqlParseException(message, sql, position);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain(message));
|
|
Assert.That(exception.Sql, Is.EqualTo(sql));
|
|
Assert.That(exception.Position, Is.EqualTo(position));
|
|
Assert.That(exception.NearText, Is.Not.Empty);
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void SqlParseException_FormatsMessageWithContext()
|
|
{
|
|
// Arrange
|
|
var message = "Invalid syntax";
|
|
var sql = "SELECT id, name FROM users WHERE active = INVALID";
|
|
var position = 40;
|
|
|
|
// Act
|
|
var exception = new SqlParseException(message, sql, position);
|
|
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain("Invalid syntax"));
|
|
Assert.That(exception.Message, Does.Contain("Position: 40"));
|
|
Assert.That(exception.Message, Does.Contain("Near:"));
|
|
}
|
|
|
|
[Test]
|
|
public void SqlParseException_HandlesNullSql()
|
|
{
|
|
// Arrange
|
|
var message = "SQL is null";
|
|
string? sql = null;
|
|
var position = 0;
|
|
|
|
// Act
|
|
var exception = new SqlParseException(message, sql!, position);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain("SQL is null"));
|
|
Assert.That(exception.Sql, Is.EqualTo(string.Empty));
|
|
Assert.That(exception.NearText, Is.EqualTo(string.Empty));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void SqlParseException_TruncatesLongSql()
|
|
{
|
|
// Arrange
|
|
var message = "Parse error";
|
|
var sql = new string('A', 250) + " FROM users";
|
|
var position = 100;
|
|
|
|
// Act
|
|
var exception = new SqlParseException(message, sql, position);
|
|
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain("truncated"));
|
|
}
|
|
|
|
[Test]
|
|
public void SqlParseException_WithInnerException()
|
|
{
|
|
// Arrange
|
|
var message = "Parse failed";
|
|
var sql = "SELECT * FROM users";
|
|
var position = 5;
|
|
var innerException = new InvalidOperationException("Original error");
|
|
|
|
// Act
|
|
var exception = new SqlParseException(message, sql, position, innerException);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(exception.InnerException, Is.EqualTo(innerException));
|
|
Assert.That(exception.Message, Does.Contain("Parse failed"));
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CteValidationException Tests
|
|
|
|
[Test]
|
|
public void CteValidationException_StoresProperties()
|
|
{
|
|
// Arrange
|
|
var message = "CTE name required";
|
|
var cteName = "my_cte";
|
|
var validationRule = "TableNameRequired";
|
|
|
|
// Act
|
|
var exception = new CteValidationException(message, cteName, validationRule);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain(message));
|
|
Assert.That(exception.CteName, Is.EqualTo(cteName));
|
|
Assert.That(exception.ValidationRule, Is.EqualTo(validationRule));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void CteValidationException_IncludesHintForKnownRules()
|
|
{
|
|
// Arrange
|
|
var message = "Table name is required";
|
|
var validationRule = "TableNameRequired";
|
|
|
|
// Act
|
|
var exception = new CteValidationException(message, null, validationRule);
|
|
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain("Hint:"));
|
|
Assert.That(exception.Message, Does.Contain("non-empty table name"));
|
|
}
|
|
|
|
[Test]
|
|
public void CteValidationException_HandlesUnknownRule()
|
|
{
|
|
// Arrange
|
|
var message = "Validation failed";
|
|
var validationRule = "UnknownRule123";
|
|
|
|
// Act
|
|
var exception = new CteValidationException(message, "my_cte", validationRule);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(exception.ValidationRule, Is.EqualTo(validationRule));
|
|
Assert.That(exception.Message, Does.Contain("Validation failed"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void CteValidationException_WithInnerException()
|
|
{
|
|
// Arrange
|
|
var message = "Validation error";
|
|
var cteName = "test_cte";
|
|
var validationRule = "DuplicateCteName";
|
|
var innerException = new ArgumentException("Duplicate");
|
|
|
|
// Act
|
|
var exception = new CteValidationException(message, cteName, validationRule, innerException);
|
|
|
|
// Assert
|
|
Assert.That(exception.InnerException, Is.EqualTo(innerException));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region QueryBreakdown.Parse Exception Tests
|
|
|
|
[Test]
|
|
public void Parse_WithNullSql_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
string? sql = null;
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => QueryBreakdown.Parse(sql!));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithEmptySql_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var sql = "";
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => QueryBreakdown.Parse(sql));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithWhitespaceSql_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var sql = " ";
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => QueryBreakdown.Parse(sql));
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithInvalidSql_ThrowsSqlParseException()
|
|
{
|
|
// Arrange
|
|
var sql = "INVALID SQL STATEMENT";
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<SqlParseException>(() => QueryBreakdown.Parse(sql));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception!.Sql, Is.EqualTo(sql));
|
|
Assert.That(exception.Message, Does.Contain("Failed to parse"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void Parse_WithMalformedSql_ProvidesSqlParseExceptionWithContext()
|
|
{
|
|
// Arrange - Use SQL that will definitely fail parsing
|
|
var sql = "SELECT WHERE FROM";
|
|
|
|
// Act
|
|
var exception = Assert.Throws<SqlParseException>(() => QueryBreakdown.Parse(sql));
|
|
|
|
// Assert
|
|
Assert.That(exception, Is.Not.Null);
|
|
Assert.That(exception!.Sql, Is.EqualTo(sql));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AddWithClause Validation Tests
|
|
|
|
[Test]
|
|
public void AddWithClause_WithNullTableName_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var cteQuery = new QueryBreakdown("id, name", "users");
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause(null!, cteQuery));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("TableNameRequired"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithEmptyTableName_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var cteQuery = new QueryBreakdown("id, name", "users");
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause("", cteQuery));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("TableNameRequired"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithWhitespaceTableName_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var cteQuery = new QueryBreakdown("id, name", "users");
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause(" ", cteQuery));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("TableNameRequired"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithNullQuery_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause("test_cte", (QueryBreakdown)null!));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("QueryRequired"));
|
|
Assert.That(exception.CteName, Is.EqualTo("test_cte"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithDuplicateName_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var cte1 = new QueryBreakdown("id", "users");
|
|
var cte2 = new QueryBreakdown("name", "products");
|
|
|
|
query.AddWithClause("my_cte", cte1);
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause("my_cte", cte2));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("DuplicateCteName"));
|
|
Assert.That(exception.Message, Does.Contain("already exists"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithDuplicateName_CaseInsensitive_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var cte1 = new QueryBreakdown("id", "users");
|
|
var cte2 = new QueryBreakdown("name", "products");
|
|
|
|
query.AddWithClause("MY_CTE", cte1);
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause("my_cte", cte2));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("DuplicateCteName"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithSql_NullTableName_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause(null!, "SELECT * FROM users"));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("TableNameRequired"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithSql_NullSql_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause("my_cte", (string)null!));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("QueryRequired"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithSql_InvalidSql_ThrowsSqlParseException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<SqlParseException>(() =>
|
|
query.AddWithClause("my_cte", "INVALID SQL"));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception!.Message, Does.Contain("my_cte"));
|
|
Assert.That(exception.Sql, Is.EqualTo("INVALID SQL"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithIWithClause_NullClause_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause((WithClause)null!));
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("QueryRequired"));
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_WithIWithClause_NoQueryOrSql_ThrowsCteValidationException()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var withClause = new WithClause
|
|
{
|
|
TableName = "test_cte"
|
|
// Query and Sql are both null
|
|
};
|
|
|
|
// Act & Assert
|
|
var exception = Assert.Throws<CteValidationException>(() =>
|
|
query.AddWithClause(withClause));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(exception!.ValidationRule, Is.EqualTo("QueryRequired"));
|
|
Assert.That(exception.Message, Does.Contain("Query or Sql"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void AddWithClause_ValidCte_Succeeds()
|
|
{
|
|
// Arrange
|
|
var query = new QueryBreakdown();
|
|
var cteQuery = new QueryBreakdown("id, name", "users", "active = 1");
|
|
|
|
// Act
|
|
query.AddWithClause("active_users", cteQuery);
|
|
|
|
// Assert
|
|
Assert.That(query.WithClauses, Has.Count.EqualTo(1));
|
|
Assert.That(query.WithClauses[0].TableName, Is.EqualTo("active_users"));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Exception Message Quality Tests
|
|
|
|
[Test]
|
|
public void CteValidationException_TableNameRequired_HasHelpfulMessage()
|
|
{
|
|
// Act
|
|
var exception = new CteValidationException(
|
|
"CTE table name cannot be null",
|
|
null,
|
|
"TableNameRequired");
|
|
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain("non-empty table name"));
|
|
Assert.That(exception.Message, Does.Contain("Hint:"));
|
|
}
|
|
|
|
[Test]
|
|
public void CteValidationException_DuplicateCteName_HasHelpfulMessage()
|
|
{
|
|
// Act
|
|
var exception = new CteValidationException(
|
|
"Duplicate CTE name",
|
|
"my_cte",
|
|
"DuplicateCteName");
|
|
|
|
// Assert
|
|
Assert.That(exception.Message, Does.Contain("unique"));
|
|
Assert.That(exception.Message, Does.Contain("Hint:"));
|
|
}
|
|
|
|
[Test]
|
|
public void SqlParseException_ShowsNearText()
|
|
{
|
|
// Arrange
|
|
var sql = "SELECT id, name FROM users WHERE active = 1 AND status = 'INVALID'";
|
|
var position = 50;
|
|
|
|
// Act
|
|
var exception = new SqlParseException("Syntax error", sql, position);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
// Assert
|
|
Assert.That(exception.NearText, Does.Contain("status"));
|
|
Assert.That(exception.Message, Does.Contain("Near:"));
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
}
|