Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ public sealed class HiveServer2TestServer : IDisposable
private readonly CancellationTokenSource _shutdown = new();
private readonly Task _acceptLoop;
private readonly ITAsyncProcessor _processor;
private int _requestCount;

/// <summary>
/// Optional override that lets tests force a specific HTTP status on
/// individual requests (1-indexed). Returning <see cref="HttpStatusCode.OK"/>
/// (or returning <c>null</c>, i.e. leaving this property unset) means
/// "dispatch the Thrift request normally." Anything else short-circuits
/// the handler and writes the chosen status with an empty body — the
/// driver-side <c>THttpTransport</c> surfaces that as a
/// <c>TTransportException</c> wrapping an <c>HttpRequestException</c>,
/// which is what the OpenSession-fallback path looks for.
/// </summary>
public Func<int, HttpStatusCode>? StatusCodeOverride { get; set; }

public HiveServer2TestServer(TCLIService.IAsync handler)
{
Expand Down Expand Up @@ -113,6 +126,17 @@ private async Task HandleAsync(HttpListenerContext context)
return;
}

int count = Interlocked.Increment(ref _requestCount);
if (StatusCodeOverride != null)
{
HttpStatusCode overrideCode = StatusCodeOverride(count);
if (overrideCode != HttpStatusCode.OK)
{
response.StatusCode = (int)overrideCode;
return;
}
}

using var requestBuffer = new MemoryStream();
await request.InputStream.CopyToAsync(requestBuffer, 81920, _shutdown.Token).ConfigureAwait(false);
requestBuffer.Position = 0;
Expand Down
37 changes: 27 additions & 10 deletions csharp/test/AdbcDrivers.HiveServer2.TestServer/MockResultBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,28 @@ public sealed class MockResultBuilder
private readonly List<TColumnDesc> _schemaColumns = new();
private readonly List<TColumn> _columns = new();
private int _rowCount = -1;
private int _positionBase = 1;

/// <summary>
/// Override the base index for column positions in the emitted
/// schema. Defaults to 1 (matches Hive/Spark, whose drivers use
/// <c>ColumnMapIndexOffset=1</c>). Set to 0 for Impala fixtures —
/// the Impala driver uses <c>ColumnMapIndexOffset=0</c>, so a
/// 1-indexed schema would yield off-by-one lookups when
/// PopulateColumnInfoAsync walks the column map.
/// </summary>
public MockResultBuilder WithPositionBase(int positionBase)
{
if (_schemaColumns.Count > 0)
throw new InvalidOperationException("WithPositionBase must be called before adding any columns.");
_positionBase = positionBase;
return this;
}

public MockResultBuilder Bool(string name, params bool?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.BOOLEAN_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.BOOLEAN_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
BoolVal = new TBoolColumn(
Expand All @@ -50,7 +67,7 @@ public MockResultBuilder Bool(string name, params bool?[] values)
public MockResultBuilder Tinyint(string name, params sbyte?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.TINYINT_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.TINYINT_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
ByteVal = new TByteColumn(
Expand All @@ -63,7 +80,7 @@ public MockResultBuilder Tinyint(string name, params sbyte?[] values)
public MockResultBuilder Smallint(string name, params short?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.SMALLINT_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.SMALLINT_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
I16Val = new TI16Column(
Expand All @@ -76,7 +93,7 @@ public MockResultBuilder Smallint(string name, params short?[] values)
public MockResultBuilder Int(string name, params int?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.INT_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.INT_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
I32Val = new TI32Column(
Expand All @@ -89,7 +106,7 @@ public MockResultBuilder Int(string name, params int?[] values)
public MockResultBuilder Bigint(string name, params long?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.BIGINT_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.BIGINT_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
I64Val = new TI64Column(
Expand All @@ -102,7 +119,7 @@ public MockResultBuilder Bigint(string name, params long?[] values)
public MockResultBuilder Float(string name, params float?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.FLOAT_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.FLOAT_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
DoubleVal = new TDoubleColumn(
Expand All @@ -115,7 +132,7 @@ public MockResultBuilder Float(string name, params float?[] values)
public MockResultBuilder Double(string name, params double?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.DOUBLE_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.DOUBLE_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
DoubleVal = new TDoubleColumn(
Expand Down Expand Up @@ -161,7 +178,7 @@ public MockResultBuilder Decimal(string name, int precision, int scale, params d
.Select(v => v?.ToString(CultureInfo.InvariantCulture))
.ToArray();
AssertRowCount(encoded.Length);
_schemaColumns.Add(MockSchema.DecimalColumn(name, precision, scale, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.DecimalColumn(name, precision, scale, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
StringVal = new TStringColumn(
Expand All @@ -174,7 +191,7 @@ public MockResultBuilder Decimal(string name, int precision, int scale, params d
public MockResultBuilder Binary(string name, params byte[]?[] values)
{
AssertRowCount(values.Length);
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.BINARY_TYPE, position: _schemaColumns.Count + 1));
_schemaColumns.Add(MockSchema.SimpleColumn(name, TTypeId.BINARY_TYPE, position: _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
BinaryVal = new TBinaryColumn(
Expand Down Expand Up @@ -205,7 +222,7 @@ private MockResultBuilder AddStringWireColumn(
{
new() { PrimitiveEntry = primitive },
});
_schemaColumns.Add(new TColumnDesc(name, typeDesc, _schemaColumns.Count + 1));
_schemaColumns.Add(new TColumnDesc(name, typeDesc, _schemaColumns.Count + _positionBase));
_columns.Add(new TColumn
{
StringVal = new TStringColumn(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) 2026 ADBC Drivers Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if NET5_0_OR_GREATER
using System;
using System.IO;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using AdbcDrivers.HiveServer2.Hive2;
using Xunit;

namespace AdbcDrivers.Tests.HiveServer2.Common
{
/// <summary>
/// Drives <see cref="HiveServer2TlsImpl.ValidateCertificate"/> through
/// every branch and exercises the <c>UntrustedRoot</c> arm of
/// <c>ThrowDetailedCertificateError</c> (the one ChainStatus flag a
/// self-signed cert reliably produces). The other ChainStatus arms
/// (Revoked, PartialChain, NotTimeValid, etc.) need a fabricated chain
/// with a trusted root, which isn't portable from a unit test — those
/// branches stay uncovered in this PR.
/// </summary>
public class HiveServer2TlsImplCertChainTests
{
[Fact]
public void NoPolicyErrors_ReturnsTrue()
{
// Early-return path — never inspects the cert.
Assert.True(HiveServer2TlsImpl.ValidateCertificate(
cert: null,
policyErrors: SslPolicyErrors.None,
tlsProperties: new TlsProperties()));
}

[Fact]
public void DisableServerCertificateValidation_BypassesChecks()
{
// The escape hatch — accepts anything, including a null cert.
Assert.True(HiveServer2TlsImpl.ValidateCertificate(
cert: null,
policyErrors: SslPolicyErrors.RemoteCertificateChainErrors,
tlsProperties: new TlsProperties { DisableServerCertificateValidation = true }));
}

[Fact]
public void NullCert_WithChainErrors_ReturnsFalse()
{
Assert.False(HiveServer2TlsImpl.ValidateCertificate(
cert: null,
policyErrors: SslPolicyErrors.RemoteCertificateChainErrors,
tlsProperties: new TlsProperties()));
}

[Fact]
public void NameMismatch_NotAllowed_ReturnsFalse()
{
using X509Certificate2 cert = TestCertificates.SelfSigned();
Assert.False(HiveServer2TlsImpl.ValidateCertificate(
cert,
SslPolicyErrors.RemoteCertificateNameMismatch,
new TlsProperties { AllowHostnameMismatch = false }));
}

[Fact]
public void NameMismatch_Allowed_FallsThroughToChainCheck()
{
using X509Certificate2 cert = TestCertificates.SelfSigned();
// No chain-error flag, no trusted-cert path → returns true at
// the "no chain errors" branch.
Assert.True(HiveServer2TlsImpl.ValidateCertificate(
cert,
SslPolicyErrors.RemoteCertificateNameMismatch,
new TlsProperties { AllowHostnameMismatch = true }));
}

[Fact]
public void SelfSigned_WithChainErrors_AllowSelfSigned_ReturnsTrue()
{
using X509Certificate2 cert = TestCertificates.SelfSigned();
Assert.True(HiveServer2TlsImpl.ValidateCertificate(
cert,
SslPolicyErrors.RemoteCertificateChainErrors,
new TlsProperties { AllowSelfSigned = true, RevocationMode = X509RevocationMode.NoCheck }));
}

[Fact]
public void SelfSigned_WithChainErrors_NoAllowSelfSigned_ThrowsAuthentication()
{
using X509Certificate2 cert = TestCertificates.SelfSigned();
// ThrowDetailedCertificateError fires here — for a self-signed
// cert the chain reports UntrustedRoot, hitting the first arm of
// the per-flag switch.
Assert.Throws<AuthenticationException>(() => HiveServer2TlsImpl.ValidateCertificate(
cert,
SslPolicyErrors.RemoteCertificateChainErrors,
new TlsProperties { AllowSelfSigned = false, RevocationMode = X509RevocationMode.NoCheck }));
}

[Fact]
public void TrustedCertificatePath_AllowSelfSigned_ReturnsTrue()
{
using X509Certificate2 cert = TestCertificates.SelfSigned();
string trustedPath = TestCertificates.SelfSignedAsTempFile();
try
{
Assert.True(HiveServer2TlsImpl.ValidateCertificate(
cert,
SslPolicyErrors.RemoteCertificateChainErrors,
new TlsProperties
{
AllowSelfSigned = true,
TrustedCertificatePath = trustedPath,
RevocationMode = X509RevocationMode.NoCheck,
}));
}
finally
{
File.Delete(trustedPath);
}
}

[Fact]
public void TrustedCertificatePath_ExpiredCert_NoAllowSelfSigned_Throws()
{
// ChainPolicy uses AllowUnknownCertificateAuthority so an unknown
// root doesn't fail the chain — but NotTimeValid on an expired
// cert does, regardless of flags. That's enough to fail
// customChain.Build and fire ThrowDetailedCertificateError.
using X509Certificate2 cert = TestCertificates.SelfSignedExpired("server-cert");
string trustedPath = TestCertificates.SelfSignedAsTempFile("root-cert");
try
{
Assert.Throws<AuthenticationException>(() => HiveServer2TlsImpl.ValidateCertificate(
cert,
SslPolicyErrors.RemoteCertificateChainErrors,
new TlsProperties
{
AllowSelfSigned = false,
TrustedCertificatePath = trustedPath,
RevocationMode = X509RevocationMode.NoCheck,
}));
}
finally
{
File.Delete(trustedPath);
}
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2026 ADBC Drivers Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if NET5_0_OR_GREATER
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace AdbcDrivers.Tests.HiveServer2.Common
{
/// <summary>
/// In-memory X.509 cert generation for TLS-validation tests. Builds a
/// self-signed cert via <see cref="CertificateRequest"/> — public API
/// from net5.0 onward, so this helper is conditionally compiled.
/// </summary>
internal static class TestCertificates
{
/// <summary>
/// Self-signed cert valid for one day starting now, common name
/// <c>CN=mock-server</c>. The private key isn't persisted because
/// these tests only ever validate the public cert.
/// </summary>
public static X509Certificate2 SelfSigned(string commonName = "mock-server") =>
BuildSelfSigned(commonName, DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1));

/// <summary>Self-signed cert whose validity window ended yesterday.</summary>
public static X509Certificate2 SelfSignedExpired(string commonName = "mock-server-expired") =>
BuildSelfSigned(commonName, DateTimeOffset.UtcNow.AddDays(-30), DateTimeOffset.UtcNow.AddDays(-1));

/// <summary>
/// Writes a self-signed cert to a temp <c>.cer</c> file (DER-encoded
/// public cert, no private key) and returns the path; the caller is
/// responsible for deleting it. Used to drive the
/// <c>TrustedCertificatePath</c> branch of ValidateCertificate.
/// </summary>
public static string SelfSignedAsTempFile(string commonName = "trusted-root")
{
using X509Certificate2 cert = SelfSigned(commonName);
string path = Path.Combine(Path.GetTempPath(), $"adbc-tls-{Guid.NewGuid():N}.cer");
File.WriteAllBytes(path, cert.Export(X509ContentType.Cert));
return path;
}
Comment thread
CurtHagenlocher marked this conversation as resolved.

private static X509Certificate2 BuildSelfSigned(string commonName, DateTimeOffset notBefore, DateTimeOffset notAfter)
{
using RSA rsa = RSA.Create(2048);
var req = new CertificateRequest(
$"CN={commonName}",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
return req.CreateSelfSigned(notBefore, notAfter);
}
}
}
#endif
Loading
Loading