Files
sql-utilities/tests/Strata.SqlTools.SqlBreakdown.Tests/Exceptions/ExceptionHandlingTests.cs
T
Thom LambandClaude Opus 4.7 67512d23e1 chore(sonar): wrap independent assertions in Assert.Multiple (NUnit2045)
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>
2026-05-27 11:59:47 -05:00

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
}