Skip to content

Commit

Permalink
Feature/make automatic header optional (#542)
Browse files Browse the repository at this point in the history
* Make UserAgent request header optional.

* Changed to configurable user agent header.

* Update src/GraphQL.Client/GraphQLHttpClientOptions.cs

Co-authored-by: Alexander Rose <[email protected]>

* Update src/GraphQL.Client/GraphQLHttpRequest.cs

Co-authored-by: Alexander Rose <[email protected]>

* Update src/GraphQL.Client/GraphQLHttpRequest.cs

Co-authored-by: Alexander Rose <[email protected]>

* Update src/GraphQL.Client/GraphQLHttpRequest.cs

Co-authored-by: Alexander Rose <[email protected]>

* Update src/GraphQL.Client/GraphQLHttpRequest.cs

* refactor test helpers to provide access to GraphQLHttpClientOptions when creating the test client

* test user agent header

---------

Co-authored-by: Jesse <[email protected]>
Co-authored-by: Ivan Maximov <[email protected]>
  • Loading branch information
3 people authored Apr 13, 2023
1 parent 4430c20 commit 9f7d593
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 61 deletions.
18 changes: 7 additions & 11 deletions src/GraphQL.Client/GraphQLHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,13 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson
_disposeHttpClient = true;
}

public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer), "please configure the JSON serializer you want to use");
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));

if (!HttpClient.DefaultRequestHeaders.UserAgent.Any())
HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString()));

_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(CreateGraphQLHttpWebSocket);
}
public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer), "please configure the JSON serializer you want to use");
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(CreateGraphQLHttpWebSocket);
}

#endregion

Expand Down
7 changes: 7 additions & 0 deletions src/GraphQL.Client/GraphQLHttpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,11 @@ public class GraphQLHttpClientOptions
/// See https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_init.
/// </summary>
public Func<GraphQLHttpClientOptions, object?> ConfigureWebSocketConnectionInitPayload { get; set; } = options => null;

/// <summary>
/// The default user agent request header.
/// Default to the GraphQL client assembly.
/// </summary>
public ProductInfoHeaderValue? DefaultUserAgentRequestHeader { get; set; }
= new ProductInfoHeaderValue(typeof(GraphQLHttpClient).Assembly.GetName().Name, typeof(GraphQLHttpClient).Assembly.GetName().Version.ToString());
}
3 changes: 3 additions & 0 deletions src/GraphQL.Client/GraphQLHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public virtual HttpRequestMessage ToHttpRequestMessage(GraphQLHttpClientOptions
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
message.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8"));

if (options.DefaultUserAgentRequestHeader != null)
message.Headers.UserAgent.Add(options.DefaultUserAgentRequestHeader);

#pragma warning disable CS0618 // Type or member is obsolete
PreprocessHttpRequestMessage(message);
#pragma warning restore CS0618 // Type or member is obsolete
Expand Down
19 changes: 18 additions & 1 deletion tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using GraphQL.Types;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace GraphQL.Client.Tests.Common.Chat.Schema;

public class ChatQuery : ObjectGraphType
{
private readonly IServiceProvider _serviceProvider;

public static readonly Dictionary<string, object> TestExtensions = new()
{
{"extension1", "hello world"},
Expand All @@ -16,8 +20,9 @@ public class ChatQuery : ObjectGraphType
public readonly ManualResetEventSlim LongRunningQueryBlocker = new ManualResetEventSlim();
public readonly ManualResetEventSlim WaitingOnQueryBlocker = new ManualResetEventSlim();

public ChatQuery(IChat chat)
public ChatQuery(IChat chat, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
Name = "ChatQuery";

Field<ListGraphType<MessageType>>("messages").Resolve(context => chat.AllMessages.Take(100));
Expand All @@ -37,5 +42,17 @@ public ChatQuery(IChat chat)
WaitingOnQueryBlocker.Reset();
return "finally returned";
});

Field<StringGraphType>("clientUserAgent")
.Resolve(context =>
{
var contextAccessor = _serviceProvider.GetRequiredService<IHttpContextAccessor>();
if (!contextAccessor.HttpContext.Request.Headers.UserAgent.Any())
{
context.Errors.Add(new ExecutionError("user agent header not set"));
return null;
}
return contextAccessor.HttpContext.Request.Headers.UserAgent.ToString();
});
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand All @@ -16,4 +16,7 @@
<ProjectReference Include="..\..\src\GraphQL.Client\GraphQL.Client.csproj" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
12 changes: 12 additions & 0 deletions tests/GraphQL.Client.Tests.Common/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"GraphQL.Client.Tests.Common": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:59034"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract class IntegrationServerTestFixture
{
public int Port { get; private set; }

public IWebHost Server { get; private set; }
public IWebHost? Server { get; private set; }

public abstract IGraphQLWebsocketJsonSerializer Serializer { get; }

Expand All @@ -27,7 +27,7 @@ public async Task CreateServer()
{
if (Server != null)
return;
Server = await WebHostHelpers.CreateServer(Port);
Server = await WebHostHelpers.CreateServer(Port).ConfigureAwait(false);
}

public async Task ShutdownServer()
Expand All @@ -40,18 +40,20 @@ public async Task ShutdownServer()
Server = null;
}

public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false)
=> GetGraphQLClient(Common.STAR_WARS_ENDPOINT, requestsViaWebsocket);
public GraphQLHttpClient GetStarWarsClient(Action<GraphQLHttpClientOptions>? configure = null)
=> GetGraphQLClient(Common.STAR_WARS_ENDPOINT, configure);

public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false)
=> GetGraphQLClient(Common.CHAT_ENDPOINT, requestsViaWebsocket);
public GraphQLHttpClient GetChatClient(Action<GraphQLHttpClientOptions>? configure = null)
=> GetGraphQLClient(Common.CHAT_ENDPOINT, configure);

private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false)
{
if (Serializer == null)
throw new InvalidOperationException("JSON serializer not configured");
return WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket, Serializer, WebsocketProtocol);
}
private GraphQLHttpClient GetGraphQLClient(string endpoint, Action<GraphQLHttpClientOptions>? configure) =>
Serializer == null
? throw new InvalidOperationException("JSON serializer not configured")
: WebHostHelpers.GetGraphQLClient(Port, endpoint, Serializer, options =>
{
configure?.Invoke(options);
options.WebSocketProtocol = WebsocketProtocol;
});
}

public class NewtonsoftGraphQLWsServerTestFixture : IntegrationServerTestFixture
Expand Down
42 changes: 9 additions & 33 deletions tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using GraphQL.Client.Http;
using GraphQL.Client.Http.Websocket;
using GraphQL.Client.Serializer.Newtonsoft;
using GraphQL.Client.Tests.Common;
using GraphQL.Client.Tests.Common.Helpers;
using IntegrationTestServer;

namespace GraphQL.Integration.Tests.Helpers;
Expand Down Expand Up @@ -31,37 +29,15 @@ public static async Task<IWebHost> CreateServer(int port)
return host;
}

public static GraphQLHttpClient GetGraphQLClient(int port, string endpoint, bool requestsViaWebsocket = false, IGraphQLWebsocketJsonSerializer serializer = null, string websocketProtocol = WebSocketProtocols.GRAPHQL_WS)
=> new GraphQLHttpClient(new GraphQLHttpClientOptions
{
EndPoint = new Uri($"http://localhost:{port}{endpoint}"),
UseWebSocketForQueriesAndMutations = requestsViaWebsocket,
WebSocketProtocol = websocketProtocol
},
serializer ?? new NewtonsoftJsonSerializer());
}

public class TestServerSetup : IDisposable
{
public TestServerSetup(IGraphQLWebsocketJsonSerializer serializer)
public static GraphQLHttpClient GetGraphQLClient(
int port,
string endpoint,
IGraphQLWebsocketJsonSerializer? serializer = null,
Action<GraphQLHttpClientOptions>? configure = null)
{
Serializer = serializer;
Port = NetworkHelpers.GetFreeTcpPortNumber();
var options = new GraphQLHttpClientOptions();
configure?.Invoke(options);
options.EndPoint = new Uri($"http://localhost:{port}{endpoint}");
return new GraphQLHttpClient(options, serializer ?? new NewtonsoftJsonSerializer());
}

public int Port { get; }

public IWebHost Server { get; set; }

public IGraphQLWebsocketJsonSerializer Serializer { get; set; }

public GraphQLHttpClient GetStarWarsClient(bool requestsViaWebsocket = false)
=> GetGraphQLClient(Common.STAR_WARS_ENDPOINT, requestsViaWebsocket);

public GraphQLHttpClient GetChatClient(bool requestsViaWebsocket = false)
=> GetGraphQLClient(Common.CHAT_ENDPOINT, requestsViaWebsocket);

private GraphQLHttpClient GetGraphQLClient(string endpoint, bool requestsViaWebsocket = false) => WebHostHelpers.GetGraphQLClient(Port, endpoint, requestsViaWebsocket);

public void Dispose() => Server?.Dispose();
}
68 changes: 68 additions & 0 deletions tests/GraphQL.Integration.Tests/UserAgentHeaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Net.Http.Headers;
using FluentAssertions;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Http;
using GraphQL.Integration.Tests.Helpers;
using Xunit;

namespace GraphQL.Integration.Tests;

public class UserAgentHeaderTests : IAsyncLifetime, IClassFixture<SystemTextJsonAutoNegotiateServerTestFixture>
{
private readonly IntegrationServerTestFixture Fixture;
private GraphQLHttpClient? ChatClient;

public UserAgentHeaderTests(SystemTextJsonAutoNegotiateServerTestFixture fixture)
{
Fixture = fixture;
}

public async Task InitializeAsync() => await Fixture.CreateServer().ConfigureAwait(false);

public Task DisposeAsync()
{
ChatClient?.Dispose();
return Task.CompletedTask;
}

[Fact]
public async void Can_set_custom_user_agent()
{
const string userAgent = "CustomUserAgent";
ChatClient = Fixture.GetChatClient(options => options.DefaultUserAgentRequestHeader = ProductInfoHeaderValue.Parse(userAgent));

var graphQLRequest = new GraphQLRequest("query clientUserAgent { clientUserAgent }");
var response = await ChatClient.SendQueryAsync(graphQLRequest, () => new { clientUserAgent = string.Empty }).ConfigureAwait(false);

response.Errors.Should().BeNull();
response.Data.clientUserAgent.Should().Be(userAgent);
}

[Fact]
public async void Default_user_agent_is_set_as_expected()
{
string? expectedUserAgent = new ProductInfoHeaderValue(
typeof(GraphQLHttpClient).Assembly.GetName().Name,
typeof(GraphQLHttpClient).Assembly.GetName().Version.ToString()).ToString();

ChatClient = Fixture.GetChatClient();

var graphQLRequest = new GraphQLRequest("query clientUserAgent { clientUserAgent }");
var response = await ChatClient.SendQueryAsync(graphQLRequest, () => new { clientUserAgent = string.Empty }).ConfigureAwait(false);

response.Errors.Should().BeNull();
response.Data.clientUserAgent.Should().Be(expectedUserAgent);
}

[Fact]
public async void No_Default_user_agent_if_set_to_null()
{
ChatClient = Fixture.GetChatClient(options => options.DefaultUserAgentRequestHeader = null);

var graphQLRequest = new GraphQLRequest("query clientUserAgent { clientUserAgent }");
var response = await ChatClient.SendQueryAsync(graphQLRequest, () => new { clientUserAgent = string.Empty }).ConfigureAwait(false);

response.Errors.Should().HaveCount(1);
response.Errors[0].Message.Should().Be("user agent header not set");
}
}
4 changes: 2 additions & 2 deletions tests/GraphQL.Integration.Tests/WebsocketTests/Base.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public abstract class Base : IAsyncLifetime
{
protected readonly ITestOutputHelper Output;
protected readonly IntegrationServerTestFixture Fixture;
protected GraphQLHttpClient ChatClient;
protected GraphQLHttpClient? ChatClient;

protected Base(ITestOutputHelper output, IntegrationServerTestFixture fixture)
{
Expand All @@ -43,7 +43,7 @@ public async Task InitializeAsync()
Fixture.Server.Services.GetService<Chat>().AddMessage(InitialMessage);

// then create the chat client
ChatClient ??= Fixture.GetChatClient(true);
ChatClient ??= Fixture.GetChatClient(options => options.UseWebSocketForQueriesAndMutations = true);
}

public Task DisposeAsync()
Expand Down
2 changes: 1 addition & 1 deletion tests/IntegrationTestServer/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public Startup(IConfiguration configuration, IWebHostEnvironment environment)
public void ConfigureServices(IServiceCollection services)
{
services.Configure<KestrelServerOptions>(options => options.AllowSynchronousIO = true);
//
services.AddHttpContextAccessor();
services.AddChatSchema();
services.AddStarWarsSchema();
services.AddGraphQL(builder => builder
Expand Down

0 comments on commit 9f7d593

Please sign in to comment.