Skip to content

Commit 6f44856

Browse files
authored
Return correct HTTP error for SQL CMK issues (#4765)
* Added correct exception when SQL returns an error for CMK issues * Added tests for SQL exception
1 parent 0739200 commit 6f44856

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

src/Microsoft.Health.Fhir.Core/Resources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.Health.Fhir.Core/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,4 +754,7 @@
754754
<value>The provided search parameter type {0} is not supported for the given expression {1}.</value>
755755
<comment>{0}: The type field in the search parameter. {1}: The expression field in the search parameter.</comment>
756756
</data>
757+
<data name="OperationFailedForCustomerManagedKey" xml:space="preserve">
758+
<value>Error occurred during an operation that is dependent on the customer-managed key. Use https://go.microsoft.com/fwlink/?linkid=2300268 to troubleshoot the issue.</value>
759+
</data>
757760
</root>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// -------------------------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4+
// -------------------------------------------------------------------------------------------------
5+
6+
using System;
7+
using System.Data;
8+
using System.Data.Common;
9+
using System.Data.SqlTypes;
10+
using System.Reflection;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
using Antlr4.Runtime.Tree;
14+
using Microsoft.Data.SqlClient;
15+
using Microsoft.Extensions.Logging;
16+
using Microsoft.Health.Fhir.Core.Exceptions;
17+
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
18+
using Microsoft.Health.Fhir.Tests.Common;
19+
using Microsoft.Health.SqlServer.Features.Storage;
20+
using Microsoft.Health.Test.Utilities;
21+
using NSubstitute;
22+
using Xunit;
23+
24+
namespace Microsoft.Health.Fhir.SqlServer.UnitTests.Features.Storage
25+
{
26+
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
27+
[Trait(Traits.Category, Categories.DataSourceValidation)]
28+
public class SqlExceptionActionProcessorTests
29+
{
30+
private readonly ILogger<SqlExceptionActionProcessor<string, SqlException>> _mockLogger;
31+
32+
public SqlExceptionActionProcessorTests()
33+
{
34+
_mockLogger = Substitute.For<ILogger<SqlExceptionActionProcessor<string, SqlException>>>();
35+
}
36+
37+
[Fact]
38+
public async Task GivenSqlTruncateException_WhenExecuting_ThenResourceSqlTruncateExceptionIsThrown()
39+
{
40+
// Arrange
41+
var sqlTruncateException = new SqlTruncateException("Truncate error");
42+
var logger = Substitute.For<ILogger<SqlExceptionActionProcessor<string, SqlTruncateException>>>();
43+
var processor = new SqlExceptionActionProcessor<string, SqlTruncateException>(logger);
44+
45+
// Act & Assert
46+
var exception = await Assert.ThrowsAsync<ResourceSqlTruncateException>(() =>
47+
processor.Execute("test-request", sqlTruncateException, CancellationToken.None));
48+
49+
Assert.Equal("Truncate error", exception.Message);
50+
51+
// Verify logger
52+
logger.Received(1).LogError(
53+
sqlTruncateException,
54+
$"A {nameof(ResourceSqlTruncateException)} occurred while executing request");
55+
}
56+
57+
[Theory]
58+
[InlineData(SqlErrorCodes.TimeoutExpired, typeof(RequestTimeoutException))]
59+
[InlineData(SqlErrorCodes.MethodNotAllowed, typeof(MethodNotAllowedException))]
60+
[InlineData(SqlErrorCodes.QueryProcessorNoQueryPlan, typeof(SqlQueryPlanException))]
61+
[InlineData(18456, typeof(LoginFailedForUserException))] // Login failed for user
62+
public async Task GivenSqlException_WhenExecuting_ThenSpecificExceptionIsThrown(int errorCode, Type expectedExceptionType)
63+
{
64+
// Arrange
65+
var sqlException = CreateSqlException(errorCode);
66+
var processor = new SqlExceptionActionProcessor<string, SqlException>(_mockLogger);
67+
68+
// Act & Assert
69+
await Assert.ThrowsAsync(expectedExceptionType, () =>
70+
processor.Execute("test-request", sqlException, CancellationToken.None));
71+
72+
// Verify logger
73+
_mockLogger.Received(1).LogError(
74+
sqlException,
75+
$"A {nameof(SqlException)} occurred while executing request");
76+
}
77+
78+
[Theory]
79+
[InlineData(SqlErrorCodes.KeyVaultCriticalError)]
80+
[InlineData(SqlErrorCodes.KeyVaultEncounteredError)]
81+
[InlineData(SqlErrorCodes.KeyVaultErrorObtainingInfo)]
82+
[InlineData(SqlErrorCodes.CannotConnectToDBInCurrentState)]
83+
public async Task GivenSqlExceptionWithCmkError_WhenExecuting_ThenCustomerManagedKeyExceptionIsThrown(int errorCode)
84+
{
85+
// Arrange
86+
var sqlException = CreateSqlException(errorCode);
87+
var processor = new SqlExceptionActionProcessor<string, SqlException>(_mockLogger);
88+
89+
// Act & Assert
90+
var exception = await Assert.ThrowsAsync<CustomerManagedKeyException>(() =>
91+
processor.Execute("test-request", sqlException, CancellationToken.None));
92+
93+
Assert.Equal(Core.Resources.OperationFailedForCustomerManagedKey, exception.Message);
94+
95+
// Verify logger
96+
_mockLogger.Received(1).LogError(
97+
sqlException,
98+
$"A {nameof(SqlException)} occurred while executing request");
99+
}
100+
101+
[Fact]
102+
public async Task GivenSqlExceptionWithUnhandledError_WhenExecuting_ThenResourceSqlExceptionIsThrown()
103+
{
104+
// Arrange
105+
var sqlException = CreateSqlException(99999);
106+
var processor = new SqlExceptionActionProcessor<string, SqlException>(_mockLogger);
107+
108+
// Act & Assert
109+
var exception = await Assert.ThrowsAsync<ResourceSqlException>(() =>
110+
processor.Execute("test-request", sqlException, CancellationToken.None));
111+
112+
Assert.Equal(Core.Resources.InternalServerError, exception.Message);
113+
114+
// Verify logger
115+
_mockLogger.Received(1).LogError(
116+
sqlException,
117+
$"A {nameof(SqlException)} occurred while executing request");
118+
}
119+
120+
/// <summary>
121+
/// Creates an SqlException with specific error number.
122+
/// This is required as SQLException has no public constructor and cannot be mocked easily.
123+
/// Useful for testing system errors that can't be generated by a user query.
124+
/// </summary>
125+
/// <param name="number">Sql exception number</param>
126+
/// <returns>sql exception</returns>
127+
private static SqlException CreateSqlException(int number)
128+
{
129+
var collectionConstructor = typeof(SqlErrorCollection)
130+
.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Array.Empty<Type>(), null);
131+
132+
var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Instance);
133+
134+
var errorCollection = (SqlErrorCollection)collectionConstructor.Invoke(null);
135+
136+
var errorConstructor = typeof(SqlError)
137+
.GetConstructor(
138+
BindingFlags.NonPublic | BindingFlags.Instance,
139+
null,
140+
new[] { typeof(int), typeof(byte), typeof(byte), typeof(string), typeof(string), typeof(string), typeof(int), typeof(uint), typeof(Exception) },
141+
null);
142+
143+
object error = errorConstructor
144+
.Invoke(new object[] { number, (byte)0, (byte)0, "server", "errMsg", "proccedure", 100, 0U, null });
145+
146+
addMethod.Invoke(errorCollection, new[] { error });
147+
148+
var constructor = typeof(SqlException)
149+
.GetConstructor(
150+
BindingFlags.NonPublic | BindingFlags.Instance, // visibility
151+
null,
152+
new[] { typeof(string), typeof(SqlErrorCollection), typeof(Exception), typeof(Guid) },
153+
null);
154+
155+
return (SqlException)constructor.Invoke(new object[] { "Error message", errorCollection, new DataException(), Guid.NewGuid() });
156+
}
157+
}
158+
}

src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlExceptionActionProcessor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public Task Execute(TRequest request, TException exception, CancellationToken ca
4949
{
5050
throw new LoginFailedForUserException(Core.Resources.InternalServerError, exception);
5151
}
52+
else if (sqlException.IsCMKError())
53+
{
54+
throw new CustomerManagedKeyException(Core.Resources.OperationFailedForCustomerManagedKey);
55+
}
5256
else
5357
{
5458
throw new ResourceSqlException(Core.Resources.InternalServerError, exception);

0 commit comments

Comments
 (0)