Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add headers support #65

Merged
merged 24 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1436aa7
Add headers store interface with default in-memory store.
calebkiage Aug 21, 2023
943294e
Add http headers handler that will work with System.Net.Http.HttpClient
calebkiage Aug 21, 2023
6ab1da0
Fix param refs
calebkiage Aug 21, 2023
bb36310
Add an extension function that will enable headers support on an inst…
calebkiage Aug 21, 2023
b1f97ee
Add test for RegisterHeadersOption
calebkiage Aug 21, 2023
d736405
Add assertions for commands with no handlers
calebkiage Aug 21, 2023
5aae5b4
Update package version
calebkiage Aug 21, 2023
80ee2b1
Cleanup code.
calebkiage Aug 22, 2023
3a29969
Add a concurrent in-memory header store for multi-threaded clients.
calebkiage Aug 22, 2023
3e54a6e
Use case-insensitive content header check
calebkiage Aug 22, 2023
ebfcf96
handle null headers collection in the header store API
calebkiage Aug 22, 2023
1d7e728
Use single InMemoryHeadersStore class with parameter for thread safety.
calebkiage Aug 23, 2023
7bee4da
Add note for ParseHeaders invariant.
calebkiage Aug 23, 2023
b1d6b33
Use read-only auto-property for IsConcurrent
calebkiage Aug 23, 2023
f7c2568
Add unit tests for NativeHttpHeadersHandler.cs
calebkiage Aug 23, 2023
aa1dc40
minor refactor
calebkiage Aug 23, 2023
c8fd5f4
Validate empty option name in RegisterHeaderOptions helper.
calebkiage Aug 23, 2023
8813c8d
Add more tests for RegisterHeadersOption convenience function
calebkiage Aug 23, 2023
9481dad
Add more tests for header store
calebkiage Aug 23, 2023
b7becc3
Use string constants for content headers
calebkiage Aug 24, 2023
95e5d36
Add extra null check on ParseHeaders function
calebkiage Aug 28, 2023
c3f0414
Fix sonarcloud warning
calebkiage Aug 28, 2023
e7f94fe
Remove ConcurrentInstance singleton property.
calebkiage Aug 31, 2023
29d5335
Merge branch 'main' into feat/headers-support
calebkiage Aug 31, 2023
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "Microsoft.Kiota.Cli.Commons.sln"
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Cli.Commons.Extensions;
using Microsoft.Kiota.Cli.Commons.Http.Headers;
using Microsoft.Kiota.Cli.Commons.IO;
using Moq;
using Xunit;

namespace Microsoft.Kiota.Cli.Commons.Tests.Extensions;

public class CommandBuilderExtensionTests {
public class CommandBuilderExtensionTests
{
[Fact]
public async Task UseRequestAdapterFuncRegistersTheGivenRequestAdapterAsync()
{
Expand Down Expand Up @@ -55,7 +58,8 @@ public async Task RegisterCommonServicesRegistersServicesAsync()
object? foundFilter = null;
object? foundPagingSvc = null;
var command = new Command("test");
command.SetHandler(ctx => {
command.SetHandler(ctx =>
{
foundFmtFactory = ctx.BindingContext.GetService(typeof(IOutputFormatterFactory));
foundFilter = ctx.BindingContext.GetService(typeof(IOutputFilter));
foundPagingSvc = ctx.BindingContext.GetService(typeof(IPagingService));
Expand All @@ -74,4 +78,90 @@ public async Task RegisterCommonServicesRegistersServicesAsync()
Assert.NotNull(foundPagingSvc);
Assert.IsType<ODataPagingService>(foundPagingSvc);
}
}

public class RegisterHeadersOptionTests
{
[Fact]
public async Task RegistersDefaultNamedHeadersOption()
{
var storeMock = new Mock<IHeadersStore>();
ICollection<string>? givenHeaders = null;
storeMock.Setup(m => m.SetHeaders(It.IsAny<ICollection<string>>()))
.Returns((ICollection<string> headers) =>
{
givenHeaders = headers;
return Enumerable.Empty<KeyValuePair<string, ICollection<string>>>();
});
var command = new Command("test");
var subcmd = new Command("sub");
command.Add(subcmd);
subcmd.SetHandler(_ => { });
var parser = new CommandLineBuilder(command)
.RegisterHeadersOption(() => storeMock.Object)
.Build();

var result = parser.Parse("test sub --headers a=b");
await result.InvokeAsync();

Assert.NotNull(givenHeaders);
Assert.Equal("a=b", givenHeaders.FirstOrDefault());
Assert.Empty(command.Options);
Assert.Single(subcmd.Options);
}

[Fact]
public async Task RegistersDefaultNamedHeadersOptionWhenNameIsEmpty()
{
var storeMock = new Mock<IHeadersStore>();
ICollection<string>? givenHeaders = null;
storeMock.Setup(m => m.SetHeaders(It.IsAny<ICollection<string>>()))
.Returns((ICollection<string> headers) =>
{
givenHeaders = headers;
return Enumerable.Empty<KeyValuePair<string, ICollection<string>>>();
});
var command = new Command("test");
var subcmd = new Command("sub");
command.Add(subcmd);
subcmd.SetHandler(_ => { });
var parser = new CommandLineBuilder(command)
.RegisterHeadersOption(() => storeMock.Object, name: string.Empty)
.Build();

var result = parser.Parse("test sub --headers a=b");
await result.InvokeAsync();

Assert.NotNull(givenHeaders);
Assert.Equal("a=b", givenHeaders.FirstOrDefault());
Assert.Empty(command.Options);
Assert.Single(subcmd.Options);
}
[Fact]
public async Task RegistersNamedHeadersOptionWhenNameIsProvided()
{
var storeMock = new Mock<IHeadersStore>();
ICollection<string>? givenHeaders = null;
storeMock.Setup(m => m.SetHeaders(It.IsAny<ICollection<string>>()))
.Returns((ICollection<string> headers) =>
{
givenHeaders = headers;
return Enumerable.Empty<KeyValuePair<string, ICollection<string>>>();
});
var command = new Command("test");
var subcmd = new Command("sub");
command.Add(subcmd);
subcmd.SetHandler(_ => { });
var parser = new CommandLineBuilder(command)
.RegisterHeadersOption(() => storeMock.Object, name: "--custom-headers")
.Build();

var result = parser.Parse("test sub --custom-headers a=b");
await result.InvokeAsync();

Assert.NotNull(givenHeaders);
Assert.Equal("a=b", givenHeaders.FirstOrDefault());
Assert.Empty(command.Options);
Assert.Single(subcmd.Options);
}
}
}
29 changes: 29 additions & 0 deletions src/Microsoft.Kiota.Cli.Commons.Tests/Fakes/TestingLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;

namespace Microsoft.Kiota.Cli.Commons.Tests.Fakes;

internal class TestingLogger<T> : ILogger<T>
{
public List<string> Messages { get; init; } = new();

public List<LogLevel> Levels { get; init; } = new();

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
Levels.Add(logLevel);
Messages.Add(state?.ToString() ?? string.Empty);
}

public bool IsEnabled(LogLevel logLevel)
{
return true;
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Linq;
using Microsoft.Kiota.Cli.Commons.Http.Headers;
using Xunit;

namespace Microsoft.Kiota.Cli.Commons.Tests.Http.Headers;

public class InMemoryHeaderStoreTests
{
public class SetHeadersFunction
{
[Fact]
public void Stores_Single_Header()
{
var header = new [] {"sample=header"};
var store = new InMemoryHeadersStore();
store.SetHeaders(header);

Assert.Single(store.GetHeaders());
Assert.Single(store.GetHeaders().First().Value);
}

[Fact]
public void Stores_Multiple_Headers()
{
var header = new [] {"sample=header", "sample2=header2", };
var store = new InMemoryHeadersStore();
store.SetHeaders(header);

Assert.NotEmpty(store.GetHeaders());
Assert.Equal(2, store.GetHeaders().Count());
Assert.Single(store.GetHeaders().First().Value);
}

[Fact]
public void Stores_Multiple_Headers_With_Matching_Key()
{
var header = new [] {"sample=header", "sample=header2", };
var store = new InMemoryHeadersStore();
store.SetHeaders(header);

Assert.NotEmpty(store.GetHeaders());
Assert.Single(store.GetHeaders());
Assert.Equal(2, store.GetHeaders().First().Value.Count);
}

[Fact]
public void Skips_Unparsed_Headers()
{
var header = new [] {"sample=", "test", string.Empty, };
var store = new InMemoryHeadersStore();
store.SetHeaders(header);

Assert.Empty(store.GetHeaders());
}

[Fact]
public void Clears_Existing_Headers()
{
var header = new [] {"sample=header", "sample2=header2", };
var store = new InMemoryHeadersStore();
store.SetHeaders(header);

var result = store.SetHeaders(new[] { "sample3=header3" });

Assert.Equal(2, result.Count());
Assert.Single(store.GetHeaders());
Assert.Single(store.GetHeaders().First().Value);

result = store.SetHeaders(Array.Empty<string>());
Assert.Single(result);
Assert.Empty(store.GetHeaders());
}
}

public class AddHeadersFunction
{
[Fact]
public void Adds_To_Existing_Headers()
{
var header = new [] {"sample=header", "sample2=header2", };
var store = new InMemoryHeadersStore();
store.SetHeaders(header);

Assert.Equal(2, store.GetHeaders().Count());
Assert.True(store.AddHeaders(new[] { "sample3=header3" }));
Assert.Equal(3, store.GetHeaders().Count());
Assert.False(store.AddHeaders(Array.Empty<string>()));
Assert.Equal(3, store.GetHeaders().Count());
}
}

public class DrainFunction
{
[Fact]
public void Clears_Existing_Headers()
{
var header = new [] {"sample=header", "sample2=header2", };
var store = new InMemoryHeadersStore();
store.AddHeaders(header);

Assert.Equal(2, store.GetHeaders().Count());

var result = store.Drain();

Assert.Equal(2, result.Count());
Assert.Empty(store.GetHeaders());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Kiota.Cli.Commons.Http;
using Microsoft.Kiota.Cli.Commons.Http.Headers;
using Microsoft.Kiota.Cli.Commons.Tests.Fakes;
using Moq;
using Xunit;

namespace Microsoft.Kiota.Cli.Commons.Tests.Http;

public class NativeHttpHeadersHandlerTests
{
public class SendAsyncFunction
{

[Fact]
public async Task AddsHeaderInStoreToMessageAsync()
{
var store = InMemoryHeadersStore.Instance;
store.SetHeaders(new[] { "a=b", "c=d" });

var handler = new NativeHttpHeadersHandler(() => store)
{
InnerHandler = new TestingRequestHandler()
};
var msg = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
var client = new HttpClient(handler);
await client.SendAsync(msg);

Assert.Equal("b", msg.Headers.NonValidated["a"].ToString());
Assert.Equal("d", msg.Headers.NonValidated["c"].ToString());
}

[Fact]
public async Task AddsContentHeaderInStoreToMessageAsync()
{
var store = InMemoryHeadersStore.Instance;
store.SetHeaders(new[] { "Content-Type=application/text", "Content-Length=0" });
var logger = new TestingLogger<NativeHttpHeadersHandler>();

var handler = new NativeHttpHeadersHandler(() => store, logger)
{
InnerHandler = new TestingRequestHandler()
};
var msg = new HttpRequestMessage(HttpMethod.Post, "http://localhost");
msg.Content = new StringContent(string.Empty);
var client = new HttpClient(handler);
await client.SendAsync(msg);

Assert.Equal("application/text", msg?.Content?.Headers?.ContentType?.ToString());
Assert.Equal("0", msg?.Content?.Headers?.ContentLength?.ToString());
Assert.Single(logger.Messages);
Assert.Equal(LogLevel.Warning, logger.Levels[0]);
Assert.Equal("The header Content-Type will replace an existing header value with application/text.", logger.Messages[0]);
}

[Fact]
public async Task LogsWarningWhenContentHeaderProvidedForNonContentMessage()
{
var store = InMemoryHeadersStore.Instance;
store.SetHeaders(new[] { "Content-Type=application/text" });
var logger = new TestingLogger<NativeHttpHeadersHandler>();

var handler = new NativeHttpHeadersHandler(() => store, logger)
{
InnerHandler = new TestingRequestHandler()
};
var msg = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
var client = new HttpClient(handler);
await client.SendAsync(msg);

Assert.Single(logger.Levels);
Assert.Single(logger.Messages);
Assert.Equal(LogLevel.Warning, logger.Levels[0]);
Assert.Equal("Could not add the content header Content-Type to the request headers", logger.Messages[0]);
}

[Fact]
public async Task LogsWarningWhenInvalidHeaderValueProvided()
{
var store = InMemoryHeadersStore.Instance;
store.SetHeaders(new[] { "invalid-header=x\nx" });
var logger = new TestingLogger<NativeHttpHeadersHandler>();

var handler = new NativeHttpHeadersHandler(() => store, logger)
{
InnerHandler = new TestingRequestHandler()
};
var msg = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
var client = new HttpClient(handler);
await client.SendAsync(msg);

Assert.Single(logger.Levels);
Assert.Single(logger.Messages);
Assert.Equal(LogLevel.Warning, logger.Levels[0]);
Assert.Equal("Could not add the header invalid-header to the request headers", logger.Messages[0]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Kiota.Cli.Commons.Tests.Http;

// Use as the inner handler for testing other delegating handlers.
internal class TestingRequestHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
Loading