blob: ca499380bac7985cded063bc7e4c195edd3a37b3 [file]
#region License
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Gremlin.Net.Driver;
using Gremlin.Net.Driver.Messages;
using Gremlin.Net.Process.Traversal;
using Gremlin.Net.Structure.IO;
using NSubstitute;
using Xunit;
namespace Gremlin.Net.UnitTest.Driver
{
public class ConnectionTests
{
private static readonly Uri TestUri = new Uri("http://localhost:8182/gremlin");
/// <summary>
/// Creates a mock HttpMessageHandler that captures the request and returns a canned response.
/// </summary>
private static (HttpClient httpClient, MockHandler handler) CreateMockHttpClient(
byte[]? responseBytes = null, string? contentEncoding = null)
{
var handler = new MockHandler(responseBytes ?? BuildMinimalResponseBytes(), contentEncoding);
var httpClient = new HttpClient(handler);
return (httpClient, handler);
}
/// <summary>
/// Builds a minimal valid 4.0 GraphBinary response: version + non-bulked + marker + status 200 + null msg + null exc.
/// </summary>
private static byte[] BuildMinimalResponseBytes()
{
using var ms = new MemoryStream();
ms.WriteByte(0x84); // version
ms.WriteByte(0x00); // non-bulked
ms.WriteByte(0xFD); // marker type code
ms.WriteByte(0x00); // marker value
WriteInt(ms, 200); // status code
ms.WriteByte(0x01); // null status message
ms.WriteByte(0x01); // null exception
return ms.ToArray();
}
private static void WriteInt(Stream stream, int value)
{
var bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
stream.Write(bytes, 0, 4);
}
[Fact]
public async Task ShouldSetContentTypeHeader()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal(SerializationTokens.GraphBinary4MimeType,
handler.CapturedRequest!.Content!.Headers.ContentType!.MediaType);
}
[Fact]
public async Task ShouldSetAcceptHeader()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Contains(handler.CapturedRequest!.Headers.Accept,
h => h.MediaType == SerializationTokens.GraphBinary4MimeType);
}
[Fact]
public async Task ShouldSendPostRequest()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal(HttpMethod.Post, handler.CapturedRequest!.Method);
}
[Fact]
public async Task ShouldSendToCorrectUri()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal(TestUri, handler.CapturedRequest!.RequestUri);
}
[Fact]
public async Task ShouldSetAcceptEncodingWhenCompressionEnabled()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { EnableCompression = true };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Contains(handler.CapturedRequest!.Headers.AcceptEncoding,
e => e.Value == "deflate");
}
[Fact]
public async Task ShouldNotSetAcceptEncodingWhenCompressionDisabled()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { EnableCompression = false };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.DoesNotContain(handler.CapturedRequest!.Headers.AcceptEncoding,
e => e.Value == "deflate");
}
[Fact]
public async Task ShouldSetUserAgentWhenEnabled()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { EnableUserAgentOnConnect = true };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.True(handler.CapturedRequest!.Headers.Contains("User-Agent"));
}
[Fact]
public async Task ShouldNotSetUserAgentWhenDisabled()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { EnableUserAgentOnConnect = false };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.False(handler.CapturedRequest!.Headers.Contains("User-Agent"));
}
[Fact]
public async Task ShouldSetBulkResultsHeaderWhenEnabled()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { BulkResults = true };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.True(handler.CapturedRequest!.Headers.Contains("bulkResults"));
Assert.Equal("true", handler.CapturedRequest.Headers.GetValues("bulkResults").First());
}
[Fact]
public async Task ShouldNotSetBulkResultsHeaderWhenDisabled()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { BulkResults = false };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.False(handler.CapturedRequest!.Headers.Contains("bulkResults"));
}
[Fact]
public async Task ShouldDecompressDeflateResponse()
{
// Compress the minimal response bytes with deflate
var originalBytes = BuildMinimalResponseBytes();
byte[] compressedBytes;
using (var compressedStream = new MemoryStream())
{
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Compress, true))
{
deflateStream.Write(originalBytes, 0, originalBytes.Length);
}
compressedBytes = compressedStream.ToArray();
}
var (httpClient, handler) = CreateMockHttpClient(compressedBytes, "deflate");
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { EnableCompression = true };
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
// Should not throw — decompression should work
var result = await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(result);
}
[Fact]
public void ShouldDisposeWithoutError()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
connection.Dispose();
// Double dispose should not throw
connection.Dispose();
}
private static RequestMessage CreateTestRequest()
{
return RequestMessage.Build("g.V()").AddG("g").Create();
}
private static IMessageSerializer CreateMockSerializer(
string mimeType = SerializationTokens.GraphBinary4MimeType)
{
return CreateMockSerializer(new List<object>(), mimeType);
}
private static IMessageSerializer CreateMockSerializer(
List<object> results,
string mimeType = SerializationTokens.GraphBinary4MimeType)
{
var serializer = Substitute.For<IMessageSerializer>();
serializer.MimeType.Returns(mimeType);
serializer.SerializeMessageAsync(Arg.Any<RequestMessage>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new byte[] { 0x84 }));
serializer.DeserializeMessageAsync(Arg.Any<byte[]>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(
new ResponseMessage<List<object>>(false, results, 200, null, null)));
return serializer;
}
[Fact]
public async Task ShouldCallInterceptorsInOrder()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var callOrder = new List<int>();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx => { callOrder.Add(1); return Task.CompletedTask; },
ctx => { callOrder.Add(2); return Task.CompletedTask; },
ctx => { callOrder.Add(3); return Task.CompletedTask; },
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient, interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.Equal(new List<int> { 1, 2, 3 }, callOrder);
}
[Fact]
public async Task ShouldPropagateInterceptorException()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var expectedException = new InvalidOperationException("interceptor failed");
var interceptors = new List<Func<HttpRequestContext, Task>>
{
_ => throw expectedException,
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient, interceptors);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.SubmitAsync<object>(CreateTestRequest()));
Assert.Same(expectedException, ex);
// HTTP request should not have been sent
Assert.Null(handler.CapturedRequest);
}
[Fact]
public async Task ShouldAllowInterceptorToModifyHeaders()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Headers["Authorization"] = "Basic dGVzdDp0ZXN0";
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient, interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.True(handler.CapturedRequest!.Headers.Contains("Authorization"));
Assert.Equal("Basic dGVzdDp0ZXN0",
handler.CapturedRequest.Headers.GetValues("Authorization").First());
}
[Fact]
public async Task ShouldSeeEarlierInterceptorModifications()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
string? observedHeader = null;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Headers["X-Custom"] = "first";
return Task.CompletedTask;
},
ctx =>
{
observedHeader = ctx.Headers.ContainsKey("X-Custom") ? ctx.Headers["X-Custom"] : null;
ctx.Headers["X-Custom"] = "second";
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient, interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.Equal("first", observedHeader);
Assert.NotNull(handler.CapturedRequest);
Assert.Equal("second",
handler.CapturedRequest!.Headers.GetValues("X-Custom").First());
}
[Fact]
public async Task ShouldSerializeBeforeInterceptorsWhenRequestSerializerProvided()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
object? observedBody = null;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
observedBody = ctx.Body;
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.IsType<byte[]>(observedBody);
}
[Fact]
public async Task ShouldPassRequestMessageWhenRequestSerializerIsNull()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
object? observedBody = null;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
observedBody = ctx.Body;
// Serialize the body so the request can proceed
ctx.Body = new byte[] { 0x84 };
ctx.Headers["Content-Type"] = "application/vnd.graphbinary-v4.0";
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, null, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.IsType<RequestMessage>(observedBody);
}
[Fact]
public async Task ShouldThrowWhenBodyIsNotByteArrayAfterInterceptors()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
// No interceptor serializes the body
var interceptors = new List<Func<HttpRequestContext, Task>>();
using var connection = new Connection(TestUri, null, serializer, settings, httpClient,
interceptors);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.SubmitAsync<object>(CreateTestRequest()));
Assert.Contains("byte[] or HttpContent", ex.Message);
Assert.Contains("RequestMessage", ex.Message);
// HTTP request should not have been sent
Assert.Null(handler.CapturedRequest);
}
[Fact]
public async Task ShouldSucceedWhenInterceptorSerializesBodyWithNullRequestSerializer()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
async ctx =>
{
if (ctx.Body is RequestMessage msg)
{
ctx.Body = await serializer.SerializeMessageAsync(msg);
ctx.Headers["Content-Type"] = "application/vnd.graphbinary-v4.0";
}
},
};
using var connection = new Connection(TestUri, null, serializer, settings, httpClient,
interceptors);
var result = await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(result);
Assert.NotNull(handler.CapturedRequest);
}
[Fact]
public async Task ShouldNotSetContentTypeWhenRequestSerializerIsNull()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
bool? hadContentType = null;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
hadContentType = ctx.Headers.ContainsKey("Content-Type");
// Serialize so the request can proceed
ctx.Body = new byte[] { 0x84 };
ctx.Headers["Content-Type"] = "application/vnd.graphbinary-v4.0";
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, null, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.False(hadContentType, "Content-Type should not be set before interceptors when requestSerializer is null");
Assert.NotNull(handler.CapturedRequest);
Assert.Equal("application/vnd.graphbinary-v4.0",
handler.CapturedRequest!.Content!.Headers.ContentType!.MediaType);
}
[Fact]
public async Task ShouldWorkWithEmptyInterceptorList()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
var result = await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(result);
Assert.NotNull(handler.CapturedRequest);
}
[Fact]
public async Task ShouldWorkWithNoInterceptorsParameter()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
var result = await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(result);
Assert.NotNull(handler.CapturedRequest);
}
[Fact]
public async Task ShouldAllowInterceptorToModifyUri()
{
var altUri = new Uri("http://other-host:9999/gremlin");
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Uri = altUri;
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal(altUri, handler.CapturedRequest!.RequestUri);
}
[Fact]
public async Task ShouldAllowInterceptorToReplaceBody()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var replacementBody = new byte[] { 0x01, 0x02, 0x03 };
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Body = replacementBody;
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
var sentBytes = await handler.CapturedRequest!.Content!.ReadAsByteArrayAsync();
Assert.Equal(replacementBody, sentBytes);
}
[Fact]
public async Task ShouldStopInterceptorChainOnException()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var secondCalled = false;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
_ => throw new InvalidOperationException("first failed"),
_ =>
{
secondCalled = true;
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.SubmitAsync<object>(CreateTestRequest()));
Assert.False(secondCalled, "Second interceptor should not run when first throws");
Assert.Null(handler.CapturedRequest);
}
[Fact]
public async Task ShouldSupportAsyncInterceptors()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptorCompleted = false;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
async ctx =>
{
// Simulate async work (e.g., fetching a token)
await Task.Delay(1);
ctx.Headers["X-Async-Header"] = "async-value";
interceptorCompleted = true;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.True(interceptorCompleted);
Assert.NotNull(handler.CapturedRequest);
Assert.Equal("async-value",
handler.CapturedRequest!.Headers.GetValues("X-Async-Header").First());
}
[Fact]
public async Task ShouldAllowInterceptorToReadSerializedBody()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
byte[]? capturedBody = null;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
capturedBody = ctx.Body as byte[];
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(capturedBody);
// The mock serializer returns { 0x84 }
Assert.Equal(new byte[] { 0x84 }, capturedBody);
}
[Fact]
public async Task ShouldWorkWithSingleInterceptor()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var called = false;
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
called = true;
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.True(called);
Assert.NotNull(handler.CapturedRequest);
}
[Fact]
public async Task ShouldAllowInterceptorToRemoveHeader()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings { EnableUserAgentOnConnect = true };
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Headers.Remove("User-Agent");
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.False(handler.CapturedRequest!.Headers.Contains("User-Agent"),
"Interceptor should be able to remove headers set by Connection");
}
[Fact]
public async Task ShouldThrowWhenBodyIsNullAfterInterceptors()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Body = null!;
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.SubmitAsync<object>(CreateTestRequest()));
Assert.Contains("null", ex.Message);
Assert.Null(handler.CapturedRequest);
}
[Fact]
public async Task ShouldPreserveMultipleInterceptorHeaderModifications()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Headers["X-First"] = "one";
return Task.CompletedTask;
},
ctx =>
{
ctx.Headers["X-Second"] = "two";
return Task.CompletedTask;
},
ctx =>
{
ctx.Headers["X-Third"] = "three";
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal("one", handler.CapturedRequest!.Headers.GetValues("X-First").First());
Assert.Equal("two", handler.CapturedRequest.Headers.GetValues("X-Second").First());
Assert.Equal("three", handler.CapturedRequest.Headers.GetValues("X-Third").First());
}
[Fact]
public async Task ShouldAllowCustomContentTypeWhenRequestSerializerIsNull()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Body = new byte[] { 0x01 };
ctx.Headers["Content-Type"] = "application/json";
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, null, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal("application/json",
handler.CapturedRequest!.Content!.Headers.ContentType!.MediaType);
}
[Fact]
public async Task ShouldUseResponseSerializerWhenRequestSerializerIsNull()
{
var (httpClient, _) = CreateMockHttpClient();
var requestSerializer = CreateMockSerializer();
var responseSerializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var interceptors = new List<Func<HttpRequestContext, Task>>
{
async ctx =>
{
if (ctx.Body is RequestMessage msg)
{
ctx.Body = await requestSerializer.SerializeMessageAsync(msg);
ctx.Headers["Content-Type"] = "application/vnd.graphbinary-v4.0";
}
},
};
using var connection = new Connection(TestUri, null, responseSerializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
// Verify the response serializer was called for deserialization
await responseSerializer.Received(1)
.DeserializeMessageAsync(Arg.Any<byte[]>(), Arg.Any<CancellationToken>());
// Verify the request serializer was NOT called by Connection (interceptor called it directly)
await requestSerializer.DidNotReceive()
.DeserializeMessageAsync(Arg.Any<byte[]>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task ShouldAcceptHttpContentBodyFromInterceptor()
{
var (httpClient, handler) = CreateMockHttpClient();
var serializer = CreateMockSerializer();
var settings = new ConnectionSettings();
var contentBytes = new byte[] { 0x01, 0x02, 0x03 };
var interceptors = new List<Func<HttpRequestContext, Task>>
{
ctx =>
{
ctx.Body = new ByteArrayContent(contentBytes);
return Task.CompletedTask;
},
};
using var connection = new Connection(TestUri, null, serializer, settings, httpClient,
interceptors);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
var sentBytes = await handler.CapturedRequest!.Content!.ReadAsByteArrayAsync();
Assert.Equal(contentBytes, sentBytes);
}
[Fact]
public async Task ShouldUseResponseSerializerMimeTypeForAcceptHeader()
{
var (httpClient, handler) = CreateMockHttpClient();
var requestSerializer = CreateMockSerializer("application/custom-request");
var responseSerializer = CreateMockSerializer("application/custom-response");
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, requestSerializer, responseSerializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Contains(handler.CapturedRequest!.Headers.Accept,
h => h.MediaType == "application/custom-response");
}
[Fact]
public async Task ShouldUseRequestSerializerMimeTypeForContentTypeHeader()
{
var (httpClient, handler) = CreateMockHttpClient();
var requestSerializer = CreateMockSerializer("application/custom-request");
var responseSerializer = CreateMockSerializer("application/custom-response");
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, requestSerializer, responseSerializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
Assert.Equal("application/custom-request",
handler.CapturedRequest!.Content!.Headers.ContentType!.MediaType);
}
[Fact]
public async Task ShouldUseDifferentMimeTypesForRequestAndResponseSerializers()
{
var (httpClient, handler) = CreateMockHttpClient();
var requestSerializer = CreateMockSerializer("application/vnd.custom-request-v1.0");
var responseSerializer = CreateMockSerializer("application/vnd.custom-response-v2.0");
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, requestSerializer, responseSerializer, settings, httpClient);
await connection.SubmitAsync<object>(CreateTestRequest());
Assert.NotNull(handler.CapturedRequest);
// Content-Type comes from request serializer
Assert.Equal("application/vnd.custom-request-v1.0",
handler.CapturedRequest!.Content!.Headers.ContentType!.MediaType);
// Accept comes from response serializer
Assert.Contains(handler.CapturedRequest.Headers.Accept,
h => h.MediaType == "application/vnd.custom-response-v2.0");
}
[Fact]
public async Task ShouldBoxNonTraverserResultsIntoTraversers()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer(new List<object> { "hello", 42 });
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
var result = await connection.SubmitAsync<Traverser>(CreateTestRequest());
var items = result.ToList();
Assert.Equal(2, items.Count);
Assert.Equal("hello", items[0].Object);
Assert.Equal(1, items[0].Bulk);
Assert.Equal(42, items[1].Object);
Assert.Equal(1, items[1].Bulk);
}
[Fact]
public async Task ShouldNotDoubleBoxTraverserResults()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer(new List<object> { new Traverser("existing", 5) });
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
var result = await connection.SubmitAsync<Traverser>(CreateTestRequest());
var item = result.Single();
Assert.Equal("existing", item.Object);
Assert.Equal(5, item.Bulk);
}
[Fact]
public async Task ShouldBoxMixedTraverserAndNonTraverserResults()
{
var (httpClient, _) = CreateMockHttpClient();
var serializer = CreateMockSerializer(new List<object> { new Traverser("already", 3), "raw" });
var settings = new ConnectionSettings();
using var connection = new Connection(TestUri, serializer, serializer, settings, httpClient);
var result = await connection.SubmitAsync<Traverser>(CreateTestRequest());
var items = result.ToList();
Assert.Equal(2, items.Count);
Assert.Equal("already", items[0].Object);
Assert.Equal(3, items[0].Bulk);
Assert.Equal("raw", items[1].Object);
Assert.Equal(1, items[1].Bulk);
}
/// <summary>
/// A test HttpMessageHandler that captures the request and returns a canned response.
/// </summary>
private class MockHandler : HttpMessageHandler
{
private readonly byte[] _responseBytes;
private readonly string? _contentEncoding;
public HttpRequestMessage? CapturedRequest { get; private set; }
public MockHandler(byte[] responseBytes, string? contentEncoding = null)
{
_responseBytes = responseBytes;
_contentEncoding = contentEncoding;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Clone the request headers before the original request is disposed
CapturedRequest = CloneRequest(request);
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(_responseBytes)
};
response.Content.Headers.ContentType =
new MediaTypeHeaderValue(SerializationTokens.GraphBinary4MimeType);
if (_contentEncoding != null)
{
response.Content.Headers.ContentEncoding.Add(_contentEncoding);
}
return Task.FromResult(response);
}
private static HttpRequestMessage CloneRequest(HttpRequestMessage original)
{
var clone = new HttpRequestMessage(original.Method, original.RequestUri);
// Copy headers
foreach (var header in original.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
// Copy content headers by creating a dummy content
if (original.Content != null)
{
var contentBytes = original.Content.ReadAsByteArrayAsync().Result;
clone.Content = new ByteArrayContent(contentBytes);
foreach (var header in original.Content.Headers)
{
clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
return clone;
}
}
}
}