Files
sqltoolsservice/test/Microsoft.SqlTools.ServiceLayer.UnitTests/AutoParameterization/SqlParameterizerTests.cs
David Shiflet 839acf67cd Convert most tools service tests to nunit (#1037)
* Remove xunit dependency from testdriver

* swap expected/actual as needed

* Convert Test.Common to nunit

* port hosting unit tests to nunit

* port batchparser integration tests to nunit

* port testdriver.tests to nunit

* fix target to copy dependency

* port servicelayer unittests to nunit

* more unit test fixes

* port integration tests to nunit

* fix test method type

* try using latest windows build for PRs

* reduce test memory use
2020-08-05 13:43:14 -04:00

275 lines
12 KiB
C#

//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using Microsoft.Data.SqlClient;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Exceptions;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using NUnit.Framework;
using static System.Linq.Enumerable;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.AutoParameterization
{
[TestFixture]
/// <summary>
/// Parameterization for Always Encrypted is a feature that automatically converts Transact-SQL variables
/// into query parameters (instances of <c>SqlParameter</c> Class). This allows the underlying .NET Framework
/// Data Provider for SQL Server to detect data targeting encrypted columns, and to encrypt such data before
/// sending it to the database.
/// Without parameterization, the .NET Framework Data Provider passes each statement in the Query Editor,
/// as a non-parameterized query. If the query contains literals or Transact-SQL variables that target encrypted columns,
/// the.NET Framework Data Provider for SQL Server won't be able to detect and encrypt them, before sending the query to the database.
/// As a result, the query will fail due to type mismatch (between the plaintext literal Transact-SQL variable and the encrypted column).
/// This class unit-tests the functionality os the Parameterization for Always Encrypted feature.
/// </summary>
public class SqlParameterizerTests
{
#region Query Parameterization Tests
/// <summary>
/// SqlParameterizer should parameterize Transact-SQL variables that meet the following pre-requisite conditions:
/// - Are declared and initialized in the same statement(inline initialization).
/// - Are initialized using a single literal.
/// </summary>
[Test]
public void SqlParameterizerShouldParameterizeValidVariables()
{
const string ssn = "795-73-9838";
const string birthday = "19990104";
const string salary = "$30000";
string sql = $@"
DECLARE @SSN CHAR(11) = '{ssn}'
DECLARE @BIRTHDAY DATE = '{birthday}'
DECLARE @SALARY MONEY = '{salary}'
SELECT * FROM [dbo].[Patients]
WHERE [SSN] = @SSN AND [BIRTHDAY] = @BIRTHDAY AND [SALARY] = @SALARY";
DbCommand command = new SqlCommand { CommandText = sql };
command.Parameterize();
Assert.AreEqual(expected: 3, actual: command.Parameters.Count);
}
/// <summary>
/// SqlParameterizer should not attempt parameterize Transact-SQL variables that do not meet the following pre-requisite conditions:
/// - Are declared and initialized in the same statement(inline initialization).
/// - Are initialized using a single literal.
/// The first variable has initialization separate from declaration and so should not be parameterized.
/// The second is using a function used instead of a literal and so should not be parameterized.
/// The third is using an expression used instead of a literal and so should not be parameterized.
/// </summary>
[Test]
public void SqlParameterizerShouldNotParameterizeInvalidVariables()
{
string sql = $@"
DECLARE @Name nvarchar(50);
SET @Name = 'Abel';
DECLARE @StartDate date = GETDATE();
DECLARE @NewSalary money = @Salary * 1.1;
SELECT * FROM [dbo].[Patients]
WHERE [Name] = @Name AND [StartDate] = @StartDate AND [NewSalary] = @NewSalary";
DbCommand command = new SqlCommand { CommandText = sql };
command.Parameterize();
Assert.AreEqual(expected: 0, actual: command.Parameters.Count);
}
/// <summary>
/// SQLDOM parser currently cannot handle very large scripts and runs out of memory.
/// Batch statements larger than 300000 characters (Approximately 600 Kb) should
/// throw <c>ParameterizationScriptTooLargeException</c>.
/// </summary>
[Test]
public void SqlParameterizerShouldThrowWhenSqlIsTooLong()
{
string sqlLength_300 = $@"
DECLARE @SSN CHAR(11) = '123-45-6789'
DECLARE @BIRTHDAY DATE = '19990104'
DECLARE @SALARY MONEY = '$30000'
SELECT * FROM [dbo].[Patients]
WHERE [N] = @SSN AND [B] = @BIRTHDAY AND [S] = @SALARY
GO";
// SQL less than or equal to 300000 should pass
string smallSql = string.Concat(Repeat(element: sqlLength_300, count: 1000));
DbCommand command1 = new SqlCommand { CommandText = smallSql };
command1.Parameterize();
// SQL greater than 300000 characters should throw
string bigSql = string.Concat(Repeat(element: sqlLength_300, count: 1100));
DbCommand command2 = new SqlCommand { CommandText = bigSql };
Assert.Throws<ParameterizationScriptTooLargeException>(() => command2.Parameterize());
}
/// <summary>
/// During parameterization, if we could not parse the SQL we will throw an <c>ParameterizationParsingException</c>.
/// Better to catch the error here than on the server.
/// </summary>
[Test]
public void SqlParameterizerShouldThrowWhenSqlIsInvalid()
{
string invalidSql = "THIS IS INVALID SQL";
string sql = string.Concat(Repeat(element: invalidSql, count: 1000));
DbCommand command = new SqlCommand { CommandText = sql };
Assert.Throws<ParameterizationParsingException>(() => command.Parameterize());
}
/// <summary>
/// While the SqlParameterizer should parameterize Transact-SQL variables that are declared and initialized
/// in the same statement(inline initialization) and are initialized using a single literal, the type of the
/// literal used for the initialization of the variable must also match the type in the variable declaration.
/// If not, a <c>ParameterizationFormatException</c> should get thrown.
/// </summary>
[Test]
public void SqlParameterizerShouldThrowWhenLiteralHasTypeMismatch()
{
// variable is declared an int but is getting set to character data
string sql = $@"
DECLARE @Number int = 'ABCDEFG'
SELECT * FROM [dbo].[Table]
WHERE [N] = @Number
GO";
DbCommand command = new SqlCommand { CommandText = sql };
Assert.Throws<ParameterizationFormatException>(() => command.Parameterize());
}
/// <summary>
/// A side effect of the parameterization process is that, when a batch script was composed
/// entirely of comments, the comments were stripped away and the <c>CommandText</c>
/// property of the <c>DbCommand</c> would be replaced with an empty string. When this happens,
/// the DbCommand object will throw an exception with the following message:
/// BeginExecuteReader: CommandText property has not been initialized
/// </summary>
[Test]
public void CommentOnlyBatchesShouldNotBeErasedFromCommandText()
{
string sql = $@"
-- ALTER TABLE BatchParameterization
-- ALTER COLUMN
-- [unique_key] [UNIQUEIDENTIFIER] NOT NULL";
DbCommand command = new SqlCommand { CommandText = sql };
command.Parameterize();
Assert.False(string.IsNullOrEmpty(command.CommandText));
Assert.AreEqual(expected: sql, actual: command.CommandText);
}
#endregion
#region Prarmeterization Codesense Tests
/// <summary>
/// When requesting a collection of <c>ScriptFileMarker</c> by calling the <c>SqlParameterizer.CodeSense</c>
/// method, if a null script is passed in, the reuslt should be an empty collection.
/// </summary>
[Test]
public void CodeSenseShouldReturnEmptyListWhenGivenANullScript()
{
string sql = null;
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
Assert.NotNull(result);
Assert.That(result, Is.Empty);
}
/// <summary>
/// When requesting a collection of <c>ScriptFileMarker</c> by calling the <c>SqlParameterizer.CodeSense</c>
/// method, if a script is passed in that contains no valid parameters, the reuslt should be an empty collection.
/// </summary>
[Test]
public void CodeSenseShouldReturnEmptyListWhenGivenAParameterlessScript()
{
// SQL with no parameters
string sql = $@"
SELECT * FROM [dbo].[Patients]
WHERE [N] = @SSN AND [B] = @BIRTHDAY AND [S] = @SALARY
GO";
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
Assert.NotNull(result);
Assert.That(result, Is.Empty);
}
/// <summary>
/// SQLDOM parser currently cannot handle very large scripts and runs out of memory.
/// SQL statements larger than 300000 characters (Approximately 600 Kb) should
/// return a max string sength code sense item. These will be returned to ADS to display to the user as intelli-sense.
/// </summary>
[Test]
public void CodeSenseShouldReturnMaxStringLengthScriptFileMarkerErrorItemWhenScriptIsTooLong()
{
// SQL length of 300 characters
string sqlLength_300 = $@"
DECLARE @SSN CHAR(11) = '123-45-6789'
DECLARE @BIRTHDAY DATE = '19990104'
DECLARE @SALARY MONEY = '$30000'
SELECT * FROM [dbo].[Patients]
WHERE [N] = @SSN AND [B] = @BIRTHDAY AND [S] = @SALARY
GO";
// Repeat the SQL 1001 times to exceed length threshold
string sql = string.Concat(Repeat(element: sqlLength_300, count: 1100));
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
string expectedMessage = SR.ScriptTooLarge(maxChars: 300000, currentChars: sql.Length);
Console.WriteLine(result[0].Message);
Assert.That(result, Is.Not.Empty);
Assert.AreEqual(expected: 1, actual: result.Count);
Assert.AreEqual(expected: ScriptFileMarkerLevel.Error, actual: result[0].Level);
Assert.AreEqual(expected: expectedMessage, actual: result[0].Message);
}
/// <summary>
/// When requesting a collection of <c>ScriptFileMarker</c> by calling the <c>SqlParameterizer.CodeSense</c>
/// method, if a script is passed in that contains 3 valid parameters, the reuslt should be a collection of
/// three informational code sense items. These will be returned to ADS to display to the user as intelli-sense.
/// </summary>
[Test]
public void CodeSenseShouldReturnInformationalCodeSenseItemsForValidParameters()
{
const string ssn = "795-73-9838";
const string birthday = "19990104";
const string salary = "$30000";
string sql = $@"
DECLARE @SSN CHAR(11) = '{ssn}'
DECLARE @BIRTHDAY DATE = '{birthday}'
DECLARE @SALARY MONEY = '{salary}'
SELECT * FROM [dbo].[Patients]
WHERE [SSN] = @SSN AND [BIRTHDAY] = @BIRTHDAY AND [SALARY] = @SALARY";
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
Assert.That(result, Is.Not.Empty);
Assert.AreEqual(expected: 3, actual: result.Count);
Assert.True(Enumerable.All(result, i => i.Level == ScriptFileMarkerLevel.Information));
}
#endregion
}
}