mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 09:59:48 -05:00
274 lines
12 KiB
C#
274 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.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
|
|
}
|
|
}
|