diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0effd78 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +trim_trailing_whitespace = true +insert_final_newline = false +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf + +[*.{csproj,json,config,yml,props}] +indent_size = 2 + +[*.sh] +end_of_line = lf + +[*.{cmd, bat}] +end_of_line = crlf \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8db427c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,37 @@ +############################### +# Git Line Endings # +############################### + +# Set default behavior to automatically normalize line endings. +* text=auto + +# Force batch scripts to always use CRLF line endings so that if a repo is accessed +# in Windows via a file share from Linux, the scripts will work. +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf + +# Force bash scripts to always use LF line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.sh text eol=lf + +############################### +# Git Large File System (LFS) # +############################### + +# Archives +*.7z filter=lfs diff=lfs merge=lfs -text +*.br filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text + +# Images +*.gif filter=lfs diff=lfs merge=lfs -text +*.ico filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.psd filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text + +# Other +*.exe filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 3e759b7..8afdcb6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files +*.rsuser *.suo *.user *.userosscache @@ -12,6 +13,9 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -19,10 +23,14 @@ [Rr]eleases/ x64/ x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ +[Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -36,9 +44,10 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -52,7 +61,12 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml @@ -60,7 +74,7 @@ StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c -*_i.h +*_h.h *.ilk *.meta *.obj @@ -77,6 +91,7 @@ StyleCopReport.xml *.tlh *.tmp *.tmp_proj +*_wpftmp.csproj *.log *.vspscc *.vssscc @@ -119,9 +134,6 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding add-in -.JustCode - # TeamCity is a build add-in _TeamCity* @@ -132,6 +144,11 @@ _TeamCity* .axoCover/* !.axoCover/settings.json +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + # Visual Studio code coverage results *.coverage *.coveragexml @@ -179,6 +196,8 @@ PublishScripts/ # NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -203,12 +222,14 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx +*.appxbundle +*.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!*.[Cc]ache/ +!?*.[Cc]ache/ # Others ClientBin/ @@ -221,7 +242,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -252,6 +273,9 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -287,12 +311,8 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ +# CodeRush personal settings +.cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ @@ -317,7 +337,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -326,5 +346,109 @@ ASALocalRun/ # NVidia Nsight GPU debugger configuration file *.nvuser -# MFractors (Xamarin productivity tool) working folder +# MFractors (Xamarin productivity tool) working folder .mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/AccessTokenClient.Extensions/AccessTokenClient.Extensions.csproj b/AccessTokenClient.Extensions/AccessTokenClient.Extensions.csproj index 78c0be7..ec20a35 100644 --- a/AccessTokenClient.Extensions/AccessTokenClient.Extensions.csproj +++ b/AccessTokenClient.Extensions/AccessTokenClient.Extensions.csproj @@ -1,8 +1,9 @@ - + - netstandard2.0;netcoreapp3.1;net5.0 - latest + net6.0 + true + enable 1.0.0 true William Applegate @@ -22,36 +23,22 @@ true - - - - - - - - - - - - - - - - - - - + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive + - + \ No newline at end of file diff --git a/AccessTokenClient.Extensions/AccessTokenClientPolicy.cs b/AccessTokenClient.Extensions/AccessTokenClientPolicy.cs index 6cd6809..054d554 100644 --- a/AccessTokenClient.Extensions/AccessTokenClientPolicy.cs +++ b/AccessTokenClient.Extensions/AccessTokenClientPolicy.cs @@ -1,38 +1,37 @@ -using Polly; +using Polly; using Polly.Extensions.Http; using System; using System.Net; using System.Net.Http; using Microsoft.Extensions.Logging; -namespace AccessTokenClient.Extensions +namespace AccessTokenClient.Extensions; + +/// +/// This static class contains policies that can be applied +/// easily to the access token client to increase resiliency. +/// +public static class AccessTokenClientPolicy { /// - /// This static class contains policies that can be applied - /// easily to the access token client to increase resiliency. + /// Returns the default retry policy for the access token client. /// - public static class AccessTokenClientPolicy + /// An optional logger instance. + /// + /// A default that can be applied to the + /// instance that is injected into the access token client. + /// This policy retries the token request in the event of transient http errors + /// (5XX and 408) as well as when a 404 is encountered. The request will be retried + /// twice, with a 1 second wait time between retries. + /// + public static IAsyncPolicy GetDefaultRetryPolicy(ILogger? logger = null) { - /// - /// Returns the default retry policy for the access token client. - /// - /// An optional logger instance. - /// - /// A default that can be applied to the - /// instance that is injected into the access token client. - /// This policy retries the token request in the event of transient http errors - /// (5XX and 408) as well as when a 404 is encountered. The request will be retried - /// twice, with a 1 second wait time between retries. - /// - public static IAsyncPolicy GetDefaultRetryPolicy(ILogger logger = null) - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .OrResult(message => message.StatusCode == HttpStatusCode.NotFound) - .WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1), (_, timespan, retryAttempt, _) => - { - logger?.LogWarning("Delaying for {delay}ms, then making retry attempt #{retry}.", timespan.TotalMilliseconds, retryAttempt); - }); - } + return HttpPolicyExtensions + .HandleTransientHttpError() + .OrResult(message => message.StatusCode == HttpStatusCode.NotFound) + .WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1), (_, timespan, retryAttempt, context) => + { + logger?.LogWarning("Delaying for {delay}ms, then making retry attempt #{retry}.", timespan.TotalMilliseconds, retryAttempt); + }); } } \ No newline at end of file diff --git a/AccessTokenClient.Extensions/ServiceCollectionExtensions.cs b/AccessTokenClient.Extensions/ServiceCollectionExtensions.cs index 6bb52e5..0510f3a 100644 --- a/AccessTokenClient.Extensions/ServiceCollectionExtensions.cs +++ b/AccessTokenClient.Extensions/ServiceCollectionExtensions.cs @@ -1,83 +1,79 @@ -using AccessTokenClient.Caching; -using AccessTokenClient.Serialization; +using AccessTokenClient.Caching; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System; using System.Net.Http; -namespace AccessTokenClient.Extensions +namespace AccessTokenClient.Extensions; + +/// +/// This static class contains an extension method used to add the +/// and dependencies to the service collection. +/// +public static class ServiceCollectionExtensions { /// - /// This static class contains an extension method used to add the - /// and dependencies to the service collection. + /// Adds the token client to the service collection. /// - public static class ServiceCollectionExtensions + /// The service collection. + /// + /// An optional action used to configure the instance that is returned + /// when the implementation is registered in the service collection via the + /// AddHttpClient extension method. This can be used to register an for + /// the token client, or to configure a retry policy for the to handle transient + /// errors that may be encountered. + /// + /// The service collection instance. + public static IServiceCollection AddAccessTokenClient(this IServiceCollection services, Action? builderAction = null) { - /// - /// Adds the token client to the service collection. - /// - /// The service collection. - /// - /// An optional action used to configure the instance that is returned - /// when the implementation is registered in the service collection via the - /// AddHttpClient extension method. This can be used to register an for - /// the token client, or to configure a retry policy for the to handle transient - /// errors that may be encountered. - /// - /// The service collection instance. - public static IServiceCollection AddAccessTokenClient(this IServiceCollection services, Action builderAction = null) + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.TryAddSingleton(); + throw new ArgumentNullException(nameof(services)); + } - var httpClientBuilder = services.AddHttpClient("AccessTokenClient.TokenClient"); + var httpClientBuilder = services.AddHttpClient("AccessTokenClient.TokenClient"); - builderAction?.Invoke(httpClientBuilder); + builderAction?.Invoke(httpClientBuilder); - return services; - } + return services; + } - /// - /// Enables caching for the token client. - /// - /// - /// An implementation of the interface to register. - /// - /// The service collection. - /// An optional action used to configure the token client cache options. - /// The service collection instance. - public static IServiceCollection AddAccessTokenClientCache(this IServiceCollection services, Action action = null) where T : ITokenResponseCache + /// + /// Enables caching for the token client. + /// + /// + /// An implementation of the interface to register. + /// + /// The service collection. + /// An optional action used to configure the token client cache options. + /// The service collection instance. + public static IServiceCollection AddAccessTokenClientCache(this IServiceCollection services, Action? action = null) where T : ITokenResponseCache + { + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + throw new ArgumentNullException(nameof(services)); + } - var options = new TokenClientCacheOptions(); - action?.Invoke(options); + var options = new TokenClientCacheOptions(); + action?.Invoke(options); - if(options.ExpirationBuffer <= 0) - { - throw new ArgumentException("The expiration buffer must be greater than 0."); - } + if (options.ExpirationBuffer <= 0) + { + throw new ArgumentException("The expiration buffer must be greater than 0."); + } - if(string.IsNullOrWhiteSpace(options.CacheKeyPrefix)) - { - throw new ArgumentException("A cache key prefix must be specified."); - } + if (string.IsNullOrWhiteSpace(options.CacheKeyPrefix)) + { + throw new ArgumentException("A cache key prefix must be specified."); + } - services.TryAddSingleton(options); - services.TryAddSingleton(); - services.TryAddSingleton(typeof(ITokenResponseCache), typeof(T)); - services.TryAddSingleton(); + services.TryAddSingleton(options); + services.TryAddSingleton(); + services.TryAddSingleton(typeof(ITokenResponseCache), typeof(T)); + services.TryAddSingleton(); - services.TryDecorate(); + services.TryDecorate(); - return services; - } + return services; } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/AccessTokenClient.Tests.csproj b/AccessTokenClient.Tests/AccessTokenClient.Tests.csproj index 8e6d3c1..6aeced4 100644 --- a/AccessTokenClient.Tests/AccessTokenClient.Tests.csproj +++ b/AccessTokenClient.Tests/AccessTokenClient.Tests.csproj @@ -1,30 +1,34 @@  - net5.0 + net6.0 + enable + enable false - 5.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,4 +39,4 @@ - + \ No newline at end of file diff --git a/AccessTokenClient.Tests/AccessTokenClientRetryIntegrationTests.cs b/AccessTokenClient.Tests/AccessTokenClientRetryIntegrationTests.cs new file mode 100644 index 0000000..c1b5a33 --- /dev/null +++ b/AccessTokenClient.Tests/AccessTokenClientRetryIntegrationTests.cs @@ -0,0 +1,60 @@ +using AccessTokenClient.Caching; +using AccessTokenClient.Extensions; +using AccessTokenClient.Tests.Helpers; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Net; +using Xunit; + +namespace AccessTokenClient.Tests; + +public class AccessTokenClientRetryIntegrationTests +{ + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + public async Task Test(HttpStatusCode statusCode) + { + var mockHandler = new MockHttpMessageHandler(string.Empty, statusCode); + + var services = new ServiceCollection() + .AddLogging() + .AddMemoryCache() + .AddAccessTokenClient(builder => + { + // Add the default retry policy: + builder.AddPolicyHandler((provider, _) => + { + var logger = provider.GetRequiredService>(); + return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); + }); + + // Set-up a delegating handler to mock the response for the test: + builder.AddHttpMessageHandler(() => mockHandler); + }) + .AddAccessTokenClientCache(); + + var client = services.BuildServiceProvider().GetRequiredService(); + + client.ShouldNotBeNull(); + + Func func = async () => await client.RequestAccessToken(new TokenRequest + { + TokenEndpoint = "https://service/token", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] + { + "scope:read", "scope:create", "scope:edit", "scope:delete" + } + }); + + await func.Should().ThrowAsync(); + + mockHandler.NumberOfCalls.Should().Be(3); + } +} \ No newline at end of file diff --git a/AccessTokenClient.Tests/DefaultAccessTokenTransformerTests.cs b/AccessTokenClient.Tests/DefaultAccessTokenTransformerTests.cs index d2d2f9a..64fec5f 100644 --- a/AccessTokenClient.Tests/DefaultAccessTokenTransformerTests.cs +++ b/AccessTokenClient.Tests/DefaultAccessTokenTransformerTests.cs @@ -3,32 +3,31 @@ using FluentAssertions; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class DefaultAccessTokenTransformerTests { - public class DefaultAccessTokenTransformerTests + [Fact] + public void EnsureConvertedValueIsTheSameAsOriginal() { - [Fact] - public void EnsureConvertedValueIsTheSameAsOriginal() - { - const string Value = "test"; + const string Value = "test"; - var transformer = new DefaultAccessTokenTransformer(); - var convertedValue = transformer.Convert(Value); + var transformer = new DefaultAccessTokenTransformer(); + var convertedValue = transformer.Convert(Value); - convertedValue.ShouldNotBeNull(); - convertedValue.Should().Be(Value); - } + convertedValue.ShouldNotBeNull(); + convertedValue.Should().Be(Value); + } - [Fact] - public void EnsureRevertedValueIsTheSameAsOriginal() - { - const string Value = "test"; + [Fact] + public void EnsureRevertedValueIsTheSameAsOriginal() + { + const string Value = "test"; - var transformer = new DefaultAccessTokenTransformer(); - var revertedValue = transformer.Revert(Value); + var transformer = new DefaultAccessTokenTransformer(); + var revertedValue = transformer.Revert(Value); - revertedValue.ShouldNotBeNull(); - revertedValue.Should().Be(Value); - } + revertedValue.ShouldNotBeNull(); + revertedValue.Should().Be(Value); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/DelegatingHandlerTests.cs b/AccessTokenClient.Tests/DelegatingHandlerTests.cs index c5631e0..3671615 100644 --- a/AccessTokenClient.Tests/DelegatingHandlerTests.cs +++ b/AccessTokenClient.Tests/DelegatingHandlerTests.cs @@ -1,75 +1,70 @@ using FluentAssertions; using Moq; -using System; using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class DelegatingHandlerTests { - public class DelegatingHandlerTests + [Fact] + public void EnsureExceptionThrownWhenOptionsAreNull() { - [Fact] - public void EnsureExceptionThrownWhenOptionsAreNull() - { - ITokenRequestOptions options = new Options(); - Action action = () => _ = new AccessTokenDelegatingHandler(options, null); + ITokenRequestOptions options = new Options(); + Action action = () => _ = new AccessTokenDelegatingHandler(options, null); - action.Should().Throw(); - } - - [Fact] - public void EnsureExceptionThrownWhenTokenClientIsNull() - { - var mockClient = new Mock(); - Action action = () => _ = new AccessTokenDelegatingHandler(null, mockClient.Object); + action.Should().Throw(); + } - action.Should().Throw(); - } + [Fact] + public void EnsureExceptionThrownWhenTokenClientIsNull() + { + var mockClient = new Mock(); + Action action = () => _ = new AccessTokenDelegatingHandler(null, mockClient.Object); - [Fact] - public async Task EnsureAccessTokenAddedToRequest() - { - ITokenRequestOptions options = new Options(); - var mockClient = new Mock(); + action.Should().Throw(); + } - mockClient - .Setup(m => m.RequestAccessToken(It.IsAny(), It.IsAny>>(), It.IsAny())) - .ReturnsAsync(new TokenResponse - { - AccessToken = "1234567890", - ExpiresIn = 3000 - }); + [Fact] + public async Task EnsureAccessTokenAddedToRequest() + { + ITokenRequestOptions options = new Options(); + var mockClient = new Mock(); - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://test.com"); - var handler = new AccessTokenDelegatingHandler(options, mockClient.Object) + mockClient + .Setup(m => m.RequestAccessToken(It.IsAny(), It.IsAny>>(), It.IsAny())) + .ReturnsAsync(new TokenResponse { - InnerHandler = new TestHandler() - }; + AccessToken = "1234567890", + ExpiresIn = 3000 + }); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://test.com"); + var handler = new AccessTokenDelegatingHandler(options, mockClient.Object) + { + InnerHandler = new TestHandler() + }; - var invoker = new HttpMessageInvoker(handler); - await invoker.SendAsync(httpRequestMessage, new CancellationToken()); - } + var invoker = new HttpMessageInvoker(handler); + await invoker.SendAsync(httpRequestMessage, new CancellationToken()); } +} - public class TestHandler : DelegatingHandler +public class TestHandler : DelegatingHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return Task.Factory.StartNew(() => new HttpResponseMessage(HttpStatusCode.OK), cancellationToken); - } + return Task.Factory.StartNew(() => new HttpResponseMessage(HttpStatusCode.OK), cancellationToken); } +} - public class Options : ITokenRequestOptions - { - public string TokenEndpoint { get; set; } +public class Options : ITokenRequestOptions +{ + public string TokenEndpoint { get; set; } - public string ClientIdentifier { get; set; } + public string ClientIdentifier { get; set; } - public string ClientSecret { get; set; } + public string ClientSecret { get; set; } - public string[] Scopes { get; set; } - } + public string[] Scopes { get; set; } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/Helpers/FluentAssertionsExtensions.cs b/AccessTokenClient.Tests/Helpers/FluentAssertionsExtensions.cs index 90b2805..8b35d5b 100644 --- a/AccessTokenClient.Tests/Helpers/FluentAssertionsExtensions.cs +++ b/AccessTokenClient.Tests/Helpers/FluentAssertionsExtensions.cs @@ -1,21 +1,20 @@ using FluentAssertions; using JetBrains.Annotations; -namespace AccessTokenClient.Tests.Helpers +namespace AccessTokenClient.Tests.Helpers; + +public static class FluentAssertionsExtensions { - public static class FluentAssertionsExtensions + /// + /// Asserts that the object to test is not null. Includes a + /// contract annotation that allows Resharper code analysis + /// to recognize accessing the object after this call is made + /// will not result in a null reference exception. + /// + /// The object to test. + [ContractAnnotation("null => stop")] + public static void ShouldNotBeNull(this object objectToTest) { - /// - /// Asserts that the object to test is not null. Includes a - /// contract annotation that allows Resharper code analysis - /// to recognize accessing the object after this call is made - /// will not result in a null reference exception. - /// - /// The object to test. - [ContractAnnotation("null => stop")] - public static void ShouldNotBeNull(this object objectToTest) - { - objectToTest.Should().NotBeNull(); - } + objectToTest.Should().NotBeNull(); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/Helpers/MockHttpMessageHandler.cs b/AccessTokenClient.Tests/Helpers/MockHttpMessageHandler.cs index af00351..df196b9 100644 --- a/AccessTokenClient.Tests/Helpers/MockHttpMessageHandler.cs +++ b/AccessTokenClient.Tests/Helpers/MockHttpMessageHandler.cs @@ -1,45 +1,41 @@ -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.Net; -namespace AccessTokenClient.Tests.Helpers +namespace AccessTokenClient.Tests.Helpers; + +public class MockHttpMessageHandler : DelegatingHandler { - public class MockHttpMessageHandler : DelegatingHandler - { - private readonly HttpStatusCode httpStatusCode; + private readonly HttpStatusCode httpStatusCode; - private readonly string response; + private readonly string? response; - /// - /// Initializes a new instance of the class. - /// - /// The response mock response to use. - /// The HTTP status code to return. - public MockHttpMessageHandler(string response, HttpStatusCode httpStatusCode) - { - this.response = response; - this.httpStatusCode = httpStatusCode; - } + /// + /// Initializes a new instance of the class. + /// + /// The response mock response to use. + /// The HTTP status code to return. + public MockHttpMessageHandler(string? response, HttpStatusCode httpStatusCode) + { + this.response = response; + this.httpStatusCode = httpStatusCode; + } + + public string? Input { get; private set; } - public string Input { get; private set; } + public int NumberOfCalls { get; private set; } - public int NumberOfCalls { get; private set; } + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + NumberOfCalls++; - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + if (request.Content != null) { - NumberOfCalls++; - - if (request.Content != null) - { - Input = await request.Content.ReadAsStringAsync(cancellationToken); - } - - return new HttpResponseMessage - { - StatusCode = httpStatusCode, - Content = new StringContent(response) - }; + Input = await request.Content.ReadAsStringAsync(cancellationToken); } + + return new HttpResponseMessage + { + StatusCode = httpStatusCode, + Content = response != null ? new StringContent(response) : null + }; } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/HttpClientBuilderExtensionsTests.cs b/AccessTokenClient.Tests/HttpClientBuilderExtensionsTests.cs index 5e8d820..6a2945b 100644 --- a/AccessTokenClient.Tests/HttpClientBuilderExtensionsTests.cs +++ b/AccessTokenClient.Tests/HttpClientBuilderExtensionsTests.cs @@ -1,52 +1,50 @@ -using AccessTokenClient.Extensions; +using AccessTokenClient.Extensions; using AccessTokenClient.Tests.Helpers; using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class HttpClientBuilderExtensionsTests { - public class HttpClientBuilderExtensionsTests + [Fact] + public void EnsureAccessTokenDelegatingHandlerRegisteredSuccessfully() { - [Fact] - public void EnsureAccessTokenDelegatingHandlerRegisteredSuccessfully() - { - IServiceCollection services = new ServiceCollection(); - - services.AddMemoryCache(); + IServiceCollection services = new ServiceCollection(); - services.AddAccessTokenClient(); + services.AddMemoryCache(); - services.AddSingleton(new TestClientTokenOptions()); + services.AddAccessTokenClient(); - services.AddHttpClient().AddClientAccessTokenHandler(); + services.AddSingleton(new TestClientTokenOptions()); - var provider = services.BuildServiceProvider(); + services.AddHttpClient().AddClientAccessTokenHandler(); - var client = provider.GetService(); + var provider = services.BuildServiceProvider(); - client.ShouldNotBeNull(); - } + var client = provider.GetRequiredService(); - private class TestClient - { - private readonly HttpClient client; + client.ShouldNotBeNull(); + } - public TestClient(HttpClient client) - { - this.client = client; - } - } + private class TestClient + { + private readonly HttpClient client; - private class TestClientTokenOptions : ITokenRequestOptions + public TestClient(HttpClient client) { - public string TokenEndpoint { get; set; } + this.client = client; + } + } - public string ClientIdentifier { get; set; } + private class TestClientTokenOptions : ITokenRequestOptions + { + public string TokenEndpoint { get; set; } + + public string ClientIdentifier { get; set; } - public string ClientSecret { get; set; } + public string ClientSecret { get; set; } - public string[] Scopes { get; set; } - } + public string[] Scopes { get; set; } } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/MemoryTokenResponseCache.cs b/AccessTokenClient.Tests/MemoryTokenResponseCache.cs index c3f3d9e..e7c1221 100644 --- a/AccessTokenClient.Tests/MemoryTokenResponseCache.cs +++ b/AccessTokenClient.Tests/MemoryTokenResponseCache.cs @@ -2,58 +2,55 @@ using AccessTokenClient.Tests.Helpers; using FluentAssertions; using Microsoft.Extensions.Caching.Memory; -using System; -using System.Threading.Tasks; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class MemoryTokenResponseCacheTests { - public class MemoryTokenResponseCacheTests + [Fact] + public void EnsureExceptionThrownWhenInjectedMemoryCacheIsNull() { - [Fact] - public void EnsureExceptionThrownWhenInjectedMemoryCacheIsNull() - { - Func creationFunction = () => new MemoryTokenResponseCache(null); - creationFunction.Should().Throw(); - } + Func creationFunction = () => new MemoryTokenResponseCache(null); + creationFunction.Should().Throw(); + } - [Fact] - public async Task EnsureSuccessResponseReturnedWhenCachedResponseExists() + [Fact] + public async Task EnsureSuccessResponseReturnedWhenCachedResponseExists() + { + const string Key = "testing-key"; + IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); + memoryCache.Set(Key, new TokenResponse { - const string Key = "testing-key"; - IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); - memoryCache.Set(Key, new TokenResponse - { - AccessToken = "1234567890", - ExpiresIn = 3000 - }); - var cache = new MemoryTokenResponseCache(memoryCache); - var tokenResponse = await cache.Get(Key); - tokenResponse.ShouldNotBeNull(); - } + AccessToken = "1234567890", + ExpiresIn = 3000 + }); + var cache = new MemoryTokenResponseCache(memoryCache); + var tokenResponse = await cache.Get(Key); + tokenResponse.ShouldNotBeNull(); + } - [Fact] - public async Task EnsureSetReturnsTrueWhenCacheSetSuccessfully() + [Fact] + public async Task EnsureSetReturnsTrueWhenCacheSetSuccessfully() + { + const string Key = "testing-key"; + IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); + var cache = new MemoryTokenResponseCache(memoryCache); + var result = await cache.Set(Key, new TokenResponse { - const string Key = "testing-key"; - IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); - var cache = new MemoryTokenResponseCache(memoryCache); - var result = await cache.Set(Key, new TokenResponse - { - AccessToken = "1234567890", - ExpiresIn = 3000 - }, TimeSpan.FromMinutes(3000)); - result.Should().BeTrue(); - } + AccessToken = "1234567890", + ExpiresIn = 3000 + }, TimeSpan.FromMinutes(3000)); + result.Should().BeTrue(); + } - [Fact] - public async Task EnsureNullReturnedWhenItemWithMatchingKeyDoesNotExist() - { - const string Key = "testing-key"; - IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); - var cache = new MemoryTokenResponseCache(memoryCache); - var result = await cache.Get(Key); - result.Should().BeNull(); - } + [Fact] + public async Task EnsureNullReturnedWhenItemWithMatchingKeyDoesNotExist() + { + const string Key = "testing-key"; + IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); + var cache = new MemoryTokenResponseCache(memoryCache); + var result = await cache.Get(Key); + result.Should().BeNull(); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/ServiceCollectionExtensionsTests.cs b/AccessTokenClient.Tests/ServiceCollectionExtensionsTests.cs index d474261..5af696f 100644 --- a/AccessTokenClient.Tests/ServiceCollectionExtensionsTests.cs +++ b/AccessTokenClient.Tests/ServiceCollectionExtensionsTests.cs @@ -1,133 +1,129 @@ -using AccessTokenClient.Caching; +using AccessTokenClient.Caching; using AccessTokenClient.Extensions; using AccessTokenClient.Tests.Helpers; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using System; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class ServiceCollectionExtensionsTests { - public class ServiceCollectionExtensionsTests + [Fact] + public void EnsureServiceProviderReturnsTokenClient() { - [Fact] - public void EnsureServiceProviderReturnsTokenClient() - { - var services = new ServiceCollection(); + var services = new ServiceCollection(); - services.AddMemoryCache(); + services.AddMemoryCache(); - services.AddAccessTokenClient().AddAccessTokenClientCache(); + services.AddAccessTokenClient().AddAccessTokenClientCache(); - var provider = services.BuildServiceProvider(); + var provider = services.BuildServiceProvider(); - var client = provider.GetService(); + var client = provider.GetRequiredService(); - client.ShouldNotBeNull(); - client.Should().BeOfType(); - } + client.ShouldNotBeNull(); + client.Should().BeOfType(); + } - [Fact] - public void EnsureServiceProviderReturnsTokenClientWhenRetryPolicySpecified() - { - var services = new ServiceCollection(); + [Fact] + public void EnsureServiceProviderReturnsTokenClientWhenRetryPolicySpecified() + { + var services = new ServiceCollection(); - services.AddAccessTokenClient(builder => - { - builder.AddPolicyHandler(_ => AccessTokenClientPolicy.GetDefaultRetryPolicy()); - }); + services.AddAccessTokenClient(builder => + { + builder.AddPolicyHandler(_ => AccessTokenClientPolicy.GetDefaultRetryPolicy()); + }); - var provider = services.BuildServiceProvider(); + var provider = services.BuildServiceProvider(); - var client = provider.GetService(); + var client = provider.GetRequiredService(); - client.ShouldNotBeNull(); - client.Should().BeOfType(); - } + client.ShouldNotBeNull(); + client.Should().BeOfType(); + } - [Fact] - public void EnsureServiceProviderReturnsTokenClientWhenRetryPolicySpecifiedWithLogger() - { - var services = new ServiceCollection(); + [Fact] + public void EnsureServiceProviderReturnsTokenClientWhenRetryPolicySpecifiedWithLogger() + { + var services = new ServiceCollection(); - services.AddLogging(); + services.AddLogging(); - services.AddAccessTokenClient(builder => + services.AddAccessTokenClient(builder => + { + builder.AddPolicyHandler((p, _) => { - builder.AddPolicyHandler((p, _) => - { - var logger = p.GetService>(); - return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); - }); + var logger = p.GetRequiredService>(); + return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); }); + }); - var provider = services.BuildServiceProvider(); + var provider = services.BuildServiceProvider(); - var client = provider.GetService(); + var client = provider.GetRequiredService(); - client.ShouldNotBeNull(); - client.Should().BeOfType(); - } + client.ShouldNotBeNull(); + client.Should().BeOfType(); + } - [Fact] - public void EnsureServiceProviderReturnsTokenClientWhenCachingDisabled() - { - var services = new ServiceCollection(); + [Fact] + public void EnsureServiceProviderReturnsTokenClientWhenCachingDisabled() + { + var services = new ServiceCollection(); - services.AddMemoryCache(); + services.AddMemoryCache(); - services.AddAccessTokenClient(); + services.AddAccessTokenClient(); - var provider = services.BuildServiceProvider(); + var provider = services.BuildServiceProvider(); - var client = provider.GetService(); + var client = provider.GetRequiredService(); - client.ShouldNotBeNull(); - client.Should().BeOfType(); - } + client.ShouldNotBeNull(); + client.Should().BeOfType(); + } - [Fact] - public void EnsureExceptionThrownWhenInvalidCachingOptionsSpecified() - { - var services = new ServiceCollection(); + [Fact] + public void EnsureExceptionThrownWhenInvalidCachingOptionsSpecified() + { + var services = new ServiceCollection(); - Action invalidPrefixAction = () => services.AddAccessTokenClient().AddAccessTokenClientCache(options => - { - options.CacheKeyPrefix = string.Empty; - }); + Action invalidPrefixAction = () => services.AddAccessTokenClient().AddAccessTokenClientCache(options => + { + options.CacheKeyPrefix = string.Empty; + }); - invalidPrefixAction.Should().Throw(); + invalidPrefixAction.Should().Throw(); - Action invalidBufferAction = () => services.AddAccessTokenClient().AddAccessTokenClientCache(options => - { - options.ExpirationBuffer = 0; - }); + Action invalidBufferAction = () => services.AddAccessTokenClient().AddAccessTokenClientCache(options => + { + options.ExpirationBuffer = 0; + }); - invalidBufferAction.Should().Throw(); - } + invalidBufferAction.Should().Throw(); + } - [Fact] - public void EnsureExceptionThrownWhenServiceCollectionNullUsingAddAccessTokenClientExtensionMethod() - { - IServiceCollection services = null; + [Fact] + public void EnsureExceptionThrownWhenServiceCollectionNullUsingAddAccessTokenClientExtensionMethod() + { + IServiceCollection? services = null; - // ReSharper disable once ExpressionIsAlwaysNull - Action action = () => services.AddAccessTokenClient(); + Action action = () => services!.AddAccessTokenClient(); - action.Should().Throw(); - } + action.Should().Throw(); + } - [Fact] - public void EnsureExceptionThrownWhenServiceCollectionNullUsingAddAccessTokenClientCacheMethod() - { - IServiceCollection services = null; + [Fact] + public void EnsureExceptionThrownWhenServiceCollectionNullUsingAddAccessTokenClientCacheMethod() + { + IServiceCollection? services = null; - // ReSharper disable once ExpressionIsAlwaysNull - Action action = () => services.AddAccessTokenClientCache(); + Action action = () => services!.AddAccessTokenClientCache(); - action.Should().Throw(); - } + action.Should().Throw(); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/TokenClientCachingDecoratorTests.cs b/AccessTokenClient.Tests/TokenClientCachingDecoratorTests.cs index 3035a74..d5f9eb4 100644 --- a/AccessTokenClient.Tests/TokenClientCachingDecoratorTests.cs +++ b/AccessTokenClient.Tests/TokenClientCachingDecoratorTests.cs @@ -1,335 +1,245 @@ -using AccessTokenClient.Caching; -using AccessTokenClient.Serialization; +using AccessTokenClient.Caching; using AccessTokenClient.Tests.Helpers; using FluentAssertions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using System; using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class TokenClientCachingDecoratorTests { - public class TokenClientCachingDecoratorTests + [Fact] + public async Task EnsureTokenResponseIsReturnedWhenCacheKeyIsFound() { - [Fact] - public async Task EnsureTokenResponseIsReturnedWhenCacheKeyIsFound() - { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; + const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - // Set-up the token response cache mock: - var cacheMock = new Mock(); - cacheMock.Setup(m => m.Get(It.IsAny(), It.IsAny())).ReturnsAsync(new TokenResponse - { - AccessToken = "1234567890" - }); + // Set-up the token response cache mock: + var cacheMock = new Mock(); + cacheMock.Setup(m => m.Get(It.IsAny(), It.IsAny())).ReturnsAsync(new TokenResponse + { + AccessToken = "1234567890" + }); - var decoratorLogger = new NullLogger(); + var decoratorLogger = new NullLogger(); - // Set-up the key generator mock: - var keyGeneratorMock = new Mock(); - keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); + // Set-up the key generator mock: + var keyGeneratorMock = new Mock(); + keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); - var mockTransformer = new Mock(); + var mockTransformer = new Mock(); - // Set-up the access token client, token client, and the caching decorator: - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + // Set-up the access token client, token client, and the caching decorator: + var tokenClient = new TokenClient(logger, httpClient); - ITokenClient cachingDecorator = new TokenClientCachingDecorator( - decoratorLogger, - tokenClient, - new TokenClientCacheOptions(), - cacheMock.Object, - keyGeneratorMock.Object, - mockTransformer.Object - ); + ITokenClient cachingDecorator = new TokenClientCachingDecorator( + decoratorLogger, + tokenClient, + new TokenClientCacheOptions(), + cacheMock.Object, + keyGeneratorMock.Object, + mockTransformer.Object + ); - var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); - - tokenResponse.ShouldNotBeNull(); - - messageHandler.NumberOfCalls.Should().Be(0); - } - - [Fact] - public async Task EnsureTokenResponseIsReturnedWhenCacheKeyIsNotFound() + var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + tokenResponse.ShouldNotBeNull(); - var decoratorLogger = new NullLogger(); + messageHandler.NumberOfCalls.Should().Be(0); + } - // Set-up the token response cache mock: - var cacheMock = new Mock(); - cacheMock.Setup(m => m.Get(It.IsAny(), It.IsAny())).ReturnsAsync((TokenResponse)null); + [Fact] + public async Task EnsureTokenResponseIsReturnedWhenCacheKeyIsNotFound() + { + const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - // Set-up the key generator mock: - var keyGeneratorMock = new Mock(); - keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - var mockTransformer = new Mock(); + var decoratorLogger = new NullLogger(); - // Set-up the token client and the caching decorator: - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + // Set-up the token response cache mock: + var cacheMock = new Mock(); + cacheMock.Setup(m => m.Get(It.IsAny(), It.IsAny())).ReturnsAsync((TokenResponse)null); - ITokenClient cachingDecorator = new TokenClientCachingDecorator( - decoratorLogger, - tokenClient, - new TokenClientCacheOptions(), - cacheMock.Object, - keyGeneratorMock.Object, - mockTransformer.Object - ); + // Set-up the key generator mock: + var keyGeneratorMock = new Mock(); + keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); - var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + var mockTransformer = new Mock(); - tokenResponse.ShouldNotBeNull(); - tokenResponse.AccessToken.ShouldNotBeNull(); - tokenResponse.AccessToken.Should().Be("1234567890"); + // Set-up the token client and the caching decorator: + var tokenClient = new TokenClient(logger, httpClient); - messageHandler.NumberOfCalls.Should().Be(1); - } + ITokenClient cachingDecorator = new TokenClientCachingDecorator( + decoratorLogger, + tokenClient, + new TokenClientCacheOptions(), + cacheMock.Object, + keyGeneratorMock.Object, + mockTransformer.Object + ); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task EnsureTokenResponseReturnedWhenCacheSetOperationIsSuccessfulOrFails(bool setResult) + var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + tokenResponse.ShouldNotBeNull(); + tokenResponse.AccessToken.ShouldNotBeNull(); + tokenResponse.AccessToken.Should().Be("1234567890"); - var decoratorLogger = new NullLogger(); + messageHandler.NumberOfCalls.Should().Be(1); + } - // Set-up the token response cache mock: - var cacheMock = new Mock(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task EnsureTokenResponseReturnedWhenCacheSetOperationIsSuccessfulOrFails(bool setResult) + { + const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - cacheMock - .Setup(m => m.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync((TokenResponse)null); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - cacheMock - .Setup(m => m.Set(It.IsAny(),It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(setResult); + var decoratorLogger = new NullLogger(); - // Set-up the key generator mock: - var keyGeneratorMock = new Mock(); - keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); + // Set-up the token response cache mock: + var cacheMock = new Mock(); - var mockTransformer = new Mock(); + cacheMock + .Setup(m => m.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync((TokenResponse?)null); - // Set-up the token client and the caching decorator: - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + cacheMock + .Setup(m => m.Set(It.IsAny(),It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(setResult); - ITokenClient cachingDecorator = new TokenClientCachingDecorator( - decoratorLogger, - tokenClient, - new TokenClientCacheOptions(), - cacheMock.Object, - keyGeneratorMock.Object, - mockTransformer.Object - ); + // Set-up the key generator mock: + var keyGeneratorMock = new Mock(); + keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); - var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + var mockTransformer = new Mock(); - tokenResponse.ShouldNotBeNull(); - tokenResponse.AccessToken.ShouldNotBeNull(); - tokenResponse.AccessToken.Should().Be("1234567890"); + // Set-up the token client and the caching decorator: + var tokenClient = new TokenClient(logger, httpClient); - messageHandler.NumberOfCalls.Should().Be(1); - } + ITokenClient cachingDecorator = new TokenClientCachingDecorator( + decoratorLogger, + tokenClient, + new TokenClientCacheOptions(), + cacheMock.Object, + keyGeneratorMock.Object, + mockTransformer.Object + ); - [Fact] - public async Task EnsureTokenResponseIsReturnedWhenScopesAreNotSpecified() + var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + tokenResponse.ShouldNotBeNull(); + tokenResponse.AccessToken.ShouldNotBeNull(); + tokenResponse.AccessToken.Should().Be("1234567890"); - var decoratorLogger = new NullLogger(); + messageHandler.NumberOfCalls.Should().Be(1); + } - // Set-up the token response cache mock: - var cacheMock = new Mock(); - cacheMock.Setup(m => m.Get(It.IsAny(), It.IsAny())).ReturnsAsync((TokenResponse)null); + [Fact] + public async Task EnsureTokenResponseIsReturnedWhenScopesAreNotSpecified() + { + const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - // Set-up the key generator mock: - var keyGeneratorMock = new Mock(); - keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - var mockTransformer = new Mock(); + var decoratorLogger = new NullLogger(); - // Set-up the token client and the caching decorator: - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + // Set-up the token response cache mock: + var cacheMock = new Mock(); + cacheMock.Setup(m => m.Get(It.IsAny(), It.IsAny())).ReturnsAsync((TokenResponse)null); - var cachingDecorator = new TokenClientCachingDecorator( - decoratorLogger, - tokenClient, - new TokenClientCacheOptions(), - cacheMock.Object, - keyGeneratorMock.Object, - mockTransformer.Object - ); + // Set-up the key generator mock: + var keyGeneratorMock = new Mock(); + keyGeneratorMock.Setup(m => m.GenerateTokenRequestKey(It.IsAny(), It.IsAny())).Returns("KEY-123"); - var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - }); + var mockTransformer = new Mock(); - tokenResponse.ShouldNotBeNull(); - tokenResponse.AccessToken.ShouldNotBeNull(); - tokenResponse.AccessToken.Should().Be("1234567890"); + // Set-up the token client and the caching decorator: + var tokenClient = new TokenClient(logger, httpClient); - messageHandler.NumberOfCalls.Should().Be(1); - } + var cachingDecorator = new TokenClientCachingDecorator( + decoratorLogger, + tokenClient, + new TokenClientCacheOptions(), + cacheMock.Object, + keyGeneratorMock.Object, + mockTransformer.Object + ); - [Fact] - public void EnsureExceptionThrownWhenCancellationRequested() - { - Func> creationAction = async () => - { - // Cancel the token before executing the decorator so it throws immediately: - var source = new CancellationTokenSource(); - source.Cancel(); - - var decorator = new TokenClientCachingDecorator( - new NullLogger(), - new TokenClient(new NullLogger(), new HttpClient(), new ResponseDeserializer()), - new TokenClientCacheOptions(), - new MemoryTokenResponseCache(new MemoryCache(new MemoryCacheOptions())), - new TokenRequestKeyGenerator(), - new DefaultAccessTokenTransformer()); - - var response = await decorator.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }, cancellationToken: source.Token); - - return response; - }; - - creationAction.Should().Throw(); - } - - [Fact] - public void EnsureExceptionThrownWhenLoggerIsNull() + var tokenResponse = await cachingDecorator.RequestAccessToken(new TokenRequest { - Func creationAction = () => new TokenClientCachingDecorator( - null, - new TokenClient(new NullLogger(), new HttpClient(), new ResponseDeserializer()), - new TokenClientCacheOptions(), - new MemoryTokenResponseCache(new MemoryCache(new MemoryCacheOptions())), - new TokenRequestKeyGenerator(), - new DefaultAccessTokenTransformer()); + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + }); - creationAction.Should().Throw(); - } + tokenResponse.ShouldNotBeNull(); + tokenResponse.AccessToken.ShouldNotBeNull(); + tokenResponse.AccessToken.Should().Be("1234567890"); - [Fact] - public void EnsureExceptionThrownWhenTokenClientIsNull() - { - Func creationAction = () => new TokenClientCachingDecorator( - new NullLogger(), - null, - new TokenClientCacheOptions(), - new MemoryTokenResponseCache(new MemoryCache(new MemoryCacheOptions())), - new TokenRequestKeyGenerator(), - new DefaultAccessTokenTransformer()); - - creationAction.Should().Throw(); - } - - [Fact] - public void EnsureExceptionThrownWhenTokenClientCacheOptionsIsNull() - { - Func creationAction = () => new TokenClientCachingDecorator( - new NullLogger(), - new TokenClient(new NullLogger(), new HttpClient(), new ResponseDeserializer()), - null, - new MemoryTokenResponseCache(new MemoryCache(new MemoryCacheOptions())), - new TokenRequestKeyGenerator(), - new DefaultAccessTokenTransformer()); - - creationAction.Should().Throw(); - } + messageHandler.NumberOfCalls.Should().Be(1); + } - [Fact] - public void EnsureExceptionThrownWhenTokenResponseCacheIsNull() + [Fact] + public void EnsureExceptionThrownWhenCancellationRequested() + { + Func> creationAction = async () => { - Func creationAction = () => new TokenClientCachingDecorator( - new NullLogger(), - new TokenClient(new NullLogger(), new HttpClient(), new ResponseDeserializer()), - new TokenClientCacheOptions(), - null, - new TokenRequestKeyGenerator(), - new DefaultAccessTokenTransformer()); + // Cancel the token before executing the decorator so it throws immediately: + var source = new CancellationTokenSource(); + source.Cancel(); - creationAction.Should().Throw(); - } - - [Fact] - public void EnsureExceptionThrownWhenKeyGeneratorIsNull() - { - Func creationAction = () => new TokenClientCachingDecorator( + var decorator = new TokenClientCachingDecorator( new NullLogger(), - new TokenClient(new NullLogger(), new HttpClient(), new ResponseDeserializer()), + new TokenClient(new NullLogger(), new HttpClient()), new TokenClientCacheOptions(), new MemoryTokenResponseCache(new MemoryCache(new MemoryCacheOptions())), - null, + new TokenRequestKeyGenerator(), new DefaultAccessTokenTransformer()); - creationAction.Should().Throw(); - } + var response = await decorator.RequestAccessToken(new TokenRequest + { + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }, cancellationToken: source.Token); - [Fact] - public void EnsureExceptionThrownWhenTransformerIsNull() - { - Func creationAction = () => new TokenClientCachingDecorator( - new NullLogger(), - new TokenClient(new NullLogger(), new HttpClient(), new ResponseDeserializer()), - new TokenClientCacheOptions(), - new MemoryTokenResponseCache(new MemoryCache(new MemoryCacheOptions())), - new TokenRequestKeyGenerator(), - null); + return response; + }; - creationAction.Should().Throw(); - } + creationAction.Should().ThrowAsync(); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/TokenClientRetryTests.cs b/AccessTokenClient.Tests/TokenClientRetryTests.cs index e0d2bf7..8133b79 100644 --- a/AccessTokenClient.Tests/TokenClientRetryTests.cs +++ b/AccessTokenClient.Tests/TokenClientRetryTests.cs @@ -1,112 +1,108 @@ -using AccessTokenClient.Extensions; -using AccessTokenClient.Serialization; +using AccessTokenClient.Extensions; using AccessTokenClient.Tests.Helpers; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class TokenClientRetryTests { - public class TokenClientRetryTests + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task EnsureTokenRequestRetriedOnFailure(HttpStatusCode httpStatusCode) { - [Theory] - [InlineData(HttpStatusCode.NotFound)] - [InlineData(HttpStatusCode.RequestTimeout)] - [InlineData(HttpStatusCode.InternalServerError)] - public async Task EnsureTokenRequestRetriedOnFailure(HttpStatusCode httpStatusCode) - { - var services = new ServiceCollection(); + var services = new ServiceCollection(); - var mockHandler = new MockHttpMessageHandler(string.Empty, httpStatusCode); + var mockHandler = new MockHttpMessageHandler(string.Empty, httpStatusCode); - // Configure an http client with the default retry policy handler and a mock http message handler: - services - .AddHttpClient("TestingClient") - .AddPolicyHandler((p, _) => AccessTokenClientPolicy.GetDefaultRetryPolicy()) - .AddHttpMessageHandler(() => mockHandler); + // Configure an http client with the default retry policy handler and a mock http message handler: + services + .AddHttpClient("TestingClient") + .AddPolicyHandler((_, _) => AccessTokenClientPolicy.GetDefaultRetryPolicy()) + .AddHttpMessageHandler(() => mockHandler); - var provider = services.BuildServiceProvider(); + var provider = services.BuildServiceProvider(); - // Retrieve the configured client from the service provider: - var httpClient = provider - .GetRequiredService() - .CreateClient("TestingClient"); + // Retrieve the configured client from the service provider: + var httpClient = provider + .GetRequiredService() + .CreateClient("TestingClient"); - var logger = provider.GetRequiredService>(); - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + var logger = provider.GetRequiredService>(); + var tokenClient = new TokenClient(logger, httpClient); - try - { - await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); - } - catch + try + { + await tokenClient.RequestAccessToken(new TokenRequest { - // Swallowed purposefully. - } - - // Currently, the retry policy is configured to retry twice, thus the total number of calls will be three: - mockHandler.NumberOfCalls.Should().Be(3); + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); } - - [Theory] - [InlineData(HttpStatusCode.NotFound)] - [InlineData(HttpStatusCode.RequestTimeout)] - [InlineData(HttpStatusCode.InternalServerError)] - public async Task EnsureTokenRequestRetriedOnFailureWhenLoggerSpecified(HttpStatusCode httpStatusCode) + catch { - var services = new ServiceCollection(); + // Swallowed purposefully. + } + + // Currently, the retry policy is configured to retry twice, thus the total number of calls will be three: + mockHandler.NumberOfCalls.Should().Be(3); + } - var mockHandler = new MockHttpMessageHandler(string.Empty, httpStatusCode); + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task EnsureTokenRequestRetriedOnFailureWhenLoggerSpecified(HttpStatusCode httpStatusCode) + { + var services = new ServiceCollection(); - services.AddLogging(); + var mockHandler = new MockHttpMessageHandler(string.Empty, httpStatusCode); - // Configure an http client with the default retry policy handler and a mock http message handler: - services - .AddHttpClient("TestingClient") - .AddPolicyHandler((p, _) => - { - var policyLogger = p.GetRequiredService>(); - return AccessTokenClientPolicy.GetDefaultRetryPolicy(policyLogger); - }) - .AddHttpMessageHandler(() => mockHandler); + services.AddLogging(); - var provider = services.BuildServiceProvider(); + // Configure an http client with the default retry policy handler and a mock http message handler: + services + .AddHttpClient("TestingClient") + .AddPolicyHandler((p, _) => + { + var policyLogger = p.GetRequiredService>(); + return AccessTokenClientPolicy.GetDefaultRetryPolicy(policyLogger); + }) + .AddHttpMessageHandler(() => mockHandler); - // Retrieve the configured client from the service provider: - var httpClient = provider - .GetRequiredService() - .CreateClient("TestingClient"); + var provider = services.BuildServiceProvider(); - var logger = provider.GetRequiredService>(); - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + // Retrieve the configured client from the service provider: + var httpClient = provider + .GetRequiredService() + .CreateClient("TestingClient"); - try - { - await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); - } - catch - { - // Swallowed purposefully. - } + var logger = provider.GetRequiredService>(); + var tokenClient = new TokenClient(logger, httpClient); - // Currently, the retry policy is configured to retry twice, thus the total number of calls will be three: - mockHandler.NumberOfCalls.Should().Be(3); + try + { + await tokenClient.RequestAccessToken(new TokenRequest + { + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); + } + catch + { + // Swallowed purposefully. } + + // Currently, the retry policy is configured to retry twice, thus the total number of calls will be three: + mockHandler.NumberOfCalls.Should().Be(3); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/TokenClientUnitTests.cs b/AccessTokenClient.Tests/TokenClientUnitTests.cs index 89c339d..6d08c22 100644 --- a/AccessTokenClient.Tests/TokenClientUnitTests.cs +++ b/AccessTokenClient.Tests/TokenClientUnitTests.cs @@ -1,320 +1,213 @@ -using AccessTokenClient.Serialization; using AccessTokenClient.Tests.Helpers; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using System; using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class TokenClientUnitTests { - public class TokenClientUnitTests + [Fact] + public async Task EnsureExceptionThrownWhenAccessTokenIsEmpty() { - [Fact] - public async Task EnsureExceptionThrownWhenAccessTokenIsEmpty() - { - const string Response = @"{""access_token"":"""",""token_type"":""Bearer"",""expires_in"":7199}"; + const string Response = @"{""access_token"":"""",""token_type"":""Bearer"",""expires_in"":7199}"; - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + var tokenClient = new TokenClient(logger, httpClient); - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); - - await function.Should().ThrowAsync(); - } - - [Fact] - public async Task EnsureExceptionThrownWhenTokenResponseIsEmpty() + Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest { - const string Response = ""; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + await function.Should().ThrowAsync(); + } - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + [Fact] + public async Task EnsureExceptionThrownWhenTokenResponseIsEmpty() + { + const string Response = ""; - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - await function.Should().ThrowAsync(); - } + var tokenClient = new TokenClient(logger, httpClient); - [Fact] - public async Task EnsureExceptionThrownWhenServerErrorReturned() + Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest { - const string Response = ""; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.InternalServerError); - var httpClient = new HttpClient(messageHandler); - - var mockDeserializer = new Mock(); - mockDeserializer.Setup(m => m.Deserialize(It.IsAny())).Returns((TokenResponse)null); + await function.Should().ThrowAsync(); + } - var tokenClient = new TokenClient(logger, httpClient, mockDeserializer.Object); + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + public async Task EnsureExceptionThrownWhenNonSuccessStatusCodeReturned(HttpStatusCode statusCode) + { + const string Response = ""; - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, statusCode); + var httpClient = new HttpClient(messageHandler); - await function.Should().ThrowAsync(); - } + var tokenClient = new TokenClient(logger, httpClient); - [Fact] - public async Task EnsureExceptionThrownWhenDeserializerReturnsNullTokenResponse() + Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); - - var mockDeserializer = new Mock(); - mockDeserializer.Setup(m => m.Deserialize(It.IsAny())).Returns((TokenResponse)null); - - var tokenClient = new TokenClient(logger, httpClient, mockDeserializer.Object); + await function.Should().ThrowAsync(); + } - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + [Fact] + public async Task EnsureExceptionThrownWhenCancellationRequested() + { + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(string.Empty, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - await function.Should().ThrowAsync(); - } + // Cancel the token before executing the token client so it throws immediately: + var source = new CancellationTokenSource(); + source.Cancel(); + + var client = new TokenClient(logger, httpClient); - [Fact] - public async Task EnsureExceptionThrownWhenDeserializerReturnsEmptyAccessToken() + Func> function = async () => { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); - - var mockDeserializer = new Mock(); - mockDeserializer.Setup(m => m.Deserialize(It.IsAny())).Returns(new TokenResponse - { - AccessToken = "", - ExpiresIn = 3000 - }); - - var tokenClient = new TokenClient(logger, httpClient, mockDeserializer.Object); - - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest + var tokenResponse = await client.RequestAccessToken(new TokenRequest { TokenEndpoint = "http://www.token-endpoint.com", ClientIdentifier = "client-identifier", ClientSecret = "client-secret", Scopes = new[] { "scope:read" } - }); + }, cancellationToken: source.Token); - await function.Should().ThrowAsync(); - } + return tokenResponse; + }; - [Fact] - public void EnsureExceptionThrownWhenLoggerIsNull() - { - var messageHandler = new MockHttpMessageHandler(string.Empty, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); - var mockDeserializer = new Mock(); - mockDeserializer.Setup(m => m.Deserialize(It.IsAny())).Returns((TokenResponse)null); - - Action action = () => - { - var _ = new TokenClient(null, httpClient, new ResponseDeserializer()); - }; - - action.Should().Throw(); - } + await function.Should().ThrowAsync(); + } - [Fact] - public void EnsureExceptionThrownWhenClientIsNull() - { - var logger = new NullLogger(); - var mockDeserializer = new Mock(); - mockDeserializer.Setup(m => m.Deserialize(It.IsAny())).Returns((TokenResponse)null); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task EnsureExceptionThrownWhenTokenEndpointNotSet(string tokenEndpoint) + { + var response = string.Empty; - Action action = () => - { - var _ = new TokenClient(logger, null, new ResponseDeserializer()); - }; + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - action.Should().Throw(); - } + var tokenClient = new TokenClient(logger, httpClient); - [Fact] - public void EnsureExceptionThrownWhenDeserializerIsNull() + Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest { - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(string.Empty, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + TokenEndpoint = tokenEndpoint, + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - Action action = () => - { - var _ = new TokenClient(logger, httpClient, null); - }; + await function.Should().ThrowAsync(); + } - action.Should().Throw(); - } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task EnsureExceptionThrownWhenClientIdentifierNotSet(string clientIdentifier) + { + var response = string.Empty; - [Fact] - public async Task EnsureExceptionThrownWhenCancellationRequested() - { - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(string.Empty, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - // Cancel the token before executing the token client so it throws immediately: - var source = new CancellationTokenSource(); - source.Cancel(); - - var client = new TokenClient(logger, httpClient, new ResponseDeserializer()); + var tokenClient = new TokenClient(logger, httpClient); - Func> function = async () => - { - var tokenResponse = await client.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }, cancellationToken: source.Token); - - return tokenResponse; - }; - - await function.Should().ThrowAsync(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task EnsureExceptionThrownWhenTokenEndpointNotSet(string tokenEndpoint) + Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":"",""token_type"":""Bearer"",""expires_in"":7199}"; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = clientIdentifier, + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + await function.Should().ThrowAsync(); + } - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task EnsureExceptionThrownWhenClientSecretNotSet(string clientSecret) + { + var response = string.Empty; - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = tokenEndpoint, - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - await function.Should().ThrowAsync(); - } + var tokenClient = new TokenClient(logger, httpClient); - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task EnsureExceptionThrownWhenClientIdentifierNotSet(string clientIdentifier) + Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":"",""token_type"":""Bearer"",""expires_in"":7199}"; + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = clientSecret, + Scopes = new[] { "scope:read" } + }); - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); + await function.Should().ThrowAsync(); + } - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); + [Fact] + public async Task EnsureTokenResponseFromOptionalFunctionIsReturned() + { + const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = clientIdentifier, - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }); + var logger = new NullLogger(); + var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); + var httpClient = new HttpClient(messageHandler); - await function.Should().ThrowAsync(); - } + var tokenClient = new TokenClient(logger, httpClient); - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task EnsureExceptionThrownWhenClientSecretNotSet(string clientSecret) + var tokenResponse = await tokenClient.RequestAccessToken(new TokenRequest { - const string Response = @"{""access_token"":"",""token_type"":""Bearer"",""expires_in"":7199}"; - - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); - - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); - - Func> function = async () => await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = clientSecret, - Scopes = new[] { "scope:read" } - }); - - await function.Should().ThrowAsync(); - } - - [Fact] - public async Task EnsureTokenResponseFromOptionalFunctionIsReturned() + TokenEndpoint = "http://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope:read" } + }, execute: request => Task.FromResult(new TokenResponse { - const string Response = @"{""access_token"":""1234567890"",""token_type"":""Bearer"",""expires_in"":7199}"; - - var logger = new NullLogger(); - var messageHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - var httpClient = new HttpClient(messageHandler); - - var tokenClient = new TokenClient(logger, httpClient, new ResponseDeserializer()); - - var tokenResponse = await tokenClient.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "http://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope:read" } - }, execute: request => Task.FromResult(new TokenResponse - { - AccessToken = "access-token", - ExpiresIn = 8000 - })); - - // Ensure the access token and expiration match what is returned from the execute func: - tokenResponse.ShouldNotBeNull(); - tokenResponse.AccessToken.Should().Be("access-token"); - tokenResponse.ExpiresIn.Should().Be(8000); - messageHandler.NumberOfCalls.Should().Be(0); - } + AccessToken = "access-token", + ExpiresIn = 8000 + })); + + // Ensure the access token and expiration match what is returned from the execute func: + tokenResponse.ShouldNotBeNull(); + tokenResponse.AccessToken.Should().Be("access-token"); + tokenResponse.ExpiresIn.Should().Be(8000); + messageHandler.NumberOfCalls.Should().Be(0); } } \ No newline at end of file diff --git a/AccessTokenClient.Tests/TokenRequestKeyGeneratorTests.cs b/AccessTokenClient.Tests/TokenRequestKeyGeneratorTests.cs index dcd82c5..246e55b 100644 --- a/AccessTokenClient.Tests/TokenRequestKeyGeneratorTests.cs +++ b/AccessTokenClient.Tests/TokenRequestKeyGeneratorTests.cs @@ -1,124 +1,122 @@ -using AccessTokenClient.Caching; +using AccessTokenClient.Caching; using AccessTokenClient.Tests.Helpers; using FluentAssertions; -using System; using Xunit; -namespace AccessTokenClient.Tests +namespace AccessTokenClient.Tests; + +public class TokenRequestKeyGeneratorTests { - public class TokenRequestKeyGeneratorTests + [Fact] + public void EnsureKeyGeneratedSuccessfully() { - [Fact] - public void EnsureKeyGeneratedSuccessfully() - { - var generator = new TokenRequestKeyGenerator(); - - var request = new TokenRequest - { - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope_1", "scope_2", "scope_3" }, - TokenEndpoint = "https://www.token-endpoint.com" - }; - - var key = generator.GenerateTokenRequestKey(request, "AccessTokenClient"); - - key.ShouldNotBeNull(); - key.Should().Contain("AccessTokenClient::"); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void EnsureExceptionThrownWhenTokenEndpointIsInvalid(string tokenEndpoint) + var generator = new TokenRequestKeyGenerator(); + + var request = new TokenRequest { - var generator = new TokenRequestKeyGenerator(); - - var request = new TokenRequest - { - TokenEndpoint = tokenEndpoint, - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope_1", "scope_2", "scope_3" } - }; - - Action action = () => generator.GenerateTokenRequestKey(request, "AccessTokenClient"); - - action.Should().Throw(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void EnsureExceptionThrownWhenClientIdentifierIsInvalid(string clientIdentifier) + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope_1", "scope_2", "scope_3" }, + TokenEndpoint = "https://www.token-endpoint.com" + }; + + var key = generator.GenerateTokenRequestKey(request, "AccessTokenClient"); + + key.ShouldNotBeNull(); + key.Should().Contain("AccessTokenClient::"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void EnsureExceptionThrownWhenTokenEndpointIsInvalid(string tokenEndpoint) + { + var generator = new TokenRequestKeyGenerator(); + + var request = new TokenRequest { - var generator = new TokenRequestKeyGenerator(); - - var request = new TokenRequest - { - TokenEndpoint = "https://www.token-endpoint.com", - ClientIdentifier = clientIdentifier, - ClientSecret = "client-secret", - Scopes = new[] { "scope_1", "scope_2", "scope_3" }, - }; - - Action action = () => generator.GenerateTokenRequestKey(request, "AccessTokenClient"); - - action.Should().Throw(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void EnsureExceptionThrownWhenClientSecretIsInvalid(string clientSecret) + TokenEndpoint = tokenEndpoint, + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope_1", "scope_2", "scope_3" } + }; + + Action action = () => generator.GenerateTokenRequestKey(request, "AccessTokenClient"); + + action.Should().Throw(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void EnsureExceptionThrownWhenClientIdentifierIsInvalid(string clientIdentifier) + { + var generator = new TokenRequestKeyGenerator(); + + var request = new TokenRequest { - var generator = new TokenRequestKeyGenerator(); + TokenEndpoint = "https://www.token-endpoint.com", + ClientIdentifier = clientIdentifier, + ClientSecret = "client-secret", + Scopes = new[] { "scope_1", "scope_2", "scope_3" }, + }; - var request = new TokenRequest - { - TokenEndpoint = "https://www.token-endpoint.com", - ClientIdentifier = "client-identifier", - ClientSecret = clientSecret, - Scopes = new[] { "scope_1", "scope_2", "scope_3" } - }; + Action action = () => generator.GenerateTokenRequestKey(request, "AccessTokenClient"); - Action action = () => generator.GenerateTokenRequestKey(request, "AccessTokenClient"); + action.Should().Throw(); + } - action.Should().Throw(); - } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void EnsureExceptionThrownWhenClientSecretIsInvalid(string clientSecret) + { + var generator = new TokenRequestKeyGenerator(); - [Fact] - public void EnsureRequestGeneratesDifferentKeysWhenCaseIsDifferent() + var request = new TokenRequest { - var generator = new TokenRequestKeyGenerator(); + TokenEndpoint = "https://www.token-endpoint.com", + ClientIdentifier = "client-identifier", + ClientSecret = clientSecret, + Scopes = new[] { "scope_1", "scope_2", "scope_3" } + }; + + Action action = () => generator.GenerateTokenRequestKey(request, "AccessTokenClient"); - var tokenRequestOne = new TokenRequest - { - ClientIdentifier = "client-identifier", - ClientSecret = "client-secret", - Scopes = new[] { "scope_1", "scope_2", "scope_3" }, - TokenEndpoint = "https://www.token-endpoint.com" - }; + action.Should().Throw(); + } + + [Fact] + public void EnsureRequestGeneratesDifferentKeysWhenCaseIsDifferent() + { + var generator = new TokenRequestKeyGenerator(); - var keyOne = generator.GenerateTokenRequestKey(tokenRequestOne, "AccessTokenClient"); + var tokenRequestOne = new TokenRequest + { + ClientIdentifier = "client-identifier", + ClientSecret = "client-secret", + Scopes = new[] { "scope_1", "scope_2", "scope_3" }, + TokenEndpoint = "https://www.token-endpoint.com" + }; + + var keyOne = generator.GenerateTokenRequestKey(tokenRequestOne, "AccessTokenClient"); - var tokenRequestTwo = new TokenRequest - { - ClientIdentifier = "CLIENT-IDENTIFIER", - ClientSecret = "CLIENT-SECRET", - Scopes = new[] { "SCOPE_1", "SCOPE_2", "SCOPE_3" }, - TokenEndpoint = "https://www.token-endpoint.com" - }; + var tokenRequestTwo = new TokenRequest + { + ClientIdentifier = "CLIENT-IDENTIFIER", + ClientSecret = "CLIENT-SECRET", + Scopes = new[] { "SCOPE_1", "SCOPE_2", "SCOPE_3" }, + TokenEndpoint = "https://www.token-endpoint.com" + }; - var keyTwo = generator.GenerateTokenRequestKey(tokenRequestTwo, "AccessTokenClient"); + var keyTwo = generator.GenerateTokenRequestKey(tokenRequestTwo, "AccessTokenClient"); - keyOne.ShouldNotBeNull(); - keyTwo.ShouldNotBeNull(); + keyOne.ShouldNotBeNull(); + keyTwo.ShouldNotBeNull(); - keyOne.Should().NotBeEquivalentTo(keyTwo); - } + keyOne.Should().NotBeEquivalentTo(keyTwo); } } \ No newline at end of file diff --git a/AccessTokenClient.sln b/AccessTokenClient.sln index 1f33e52..d3fe1dc 100644 --- a/AccessTokenClient.sln +++ b/AccessTokenClient.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29009.5 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31710.8 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projects", "Projects", "{469EAEB0-3EDA-4280-8981-6892639E50AE}" EndProject @@ -11,16 +11,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccessTokenClient.Extension EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccessTokenClient.Tests", "AccessTokenClient.Tests\AccessTokenClient.Tests.csproj", "{85484B72-AD4D-4839-B739-9D00119B7111}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingThreePointOneApplication", "Projects\TestingThreePointOneApplication\TestingThreePointOneApplication.csproj", "{296809C5-05E0-47E3-BB42-85B2034C0EFD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityServer", "Projects\IdentityServer\IdentityServer.csproj", "{925B0D68-397A-41F2-8959-89E7BA8FBAD1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingFivePointZeroApplication", "Projects\TestingFivePointZeroApplication\TestingFivePointZeroApplication.csproj", "{499BFD60-D90F-4307-B5C8-F0A9BE22EA50}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Projects\Benchmarks\Benchmarks.csproj", "{DB288744-C989-487B-B1F3-525B7085446F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "Projects\Console\Console.csproj", "{E296FB16-8A13-47C7-8604-84418813E699}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingSixPointZeroApplication", "Projects\TestingSixPointZeroApplication\TestingSixPointZeroApplication.csproj", "{48E954B7-06B4-44C2-8872-C8F98ADC5665}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,18 +37,10 @@ Global {85484B72-AD4D-4839-B739-9D00119B7111}.Debug|Any CPU.Build.0 = Debug|Any CPU {85484B72-AD4D-4839-B739-9D00119B7111}.Release|Any CPU.ActiveCfg = Release|Any CPU {85484B72-AD4D-4839-B739-9D00119B7111}.Release|Any CPU.Build.0 = Release|Any CPU - {296809C5-05E0-47E3-BB42-85B2034C0EFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {296809C5-05E0-47E3-BB42-85B2034C0EFD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {296809C5-05E0-47E3-BB42-85B2034C0EFD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {296809C5-05E0-47E3-BB42-85B2034C0EFD}.Release|Any CPU.Build.0 = Release|Any CPU {925B0D68-397A-41F2-8959-89E7BA8FBAD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {925B0D68-397A-41F2-8959-89E7BA8FBAD1}.Debug|Any CPU.Build.0 = Debug|Any CPU {925B0D68-397A-41F2-8959-89E7BA8FBAD1}.Release|Any CPU.ActiveCfg = Release|Any CPU {925B0D68-397A-41F2-8959-89E7BA8FBAD1}.Release|Any CPU.Build.0 = Release|Any CPU - {499BFD60-D90F-4307-B5C8-F0A9BE22EA50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {499BFD60-D90F-4307-B5C8-F0A9BE22EA50}.Debug|Any CPU.Build.0 = Debug|Any CPU - {499BFD60-D90F-4307-B5C8-F0A9BE22EA50}.Release|Any CPU.ActiveCfg = Release|Any CPU - {499BFD60-D90F-4307-B5C8-F0A9BE22EA50}.Release|Any CPU.Build.0 = Release|Any CPU {DB288744-C989-487B-B1F3-525B7085446F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DB288744-C989-487B-B1F3-525B7085446F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB288744-C989-487B-B1F3-525B7085446F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -59,16 +49,19 @@ Global {E296FB16-8A13-47C7-8604-84418813E699}.Debug|Any CPU.Build.0 = Debug|Any CPU {E296FB16-8A13-47C7-8604-84418813E699}.Release|Any CPU.ActiveCfg = Release|Any CPU {E296FB16-8A13-47C7-8604-84418813E699}.Release|Any CPU.Build.0 = Release|Any CPU + {48E954B7-06B4-44C2-8872-C8F98ADC5665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48E954B7-06B4-44C2-8872-C8F98ADC5665}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48E954B7-06B4-44C2-8872-C8F98ADC5665}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48E954B7-06B4-44C2-8872-C8F98ADC5665}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {296809C5-05E0-47E3-BB42-85B2034C0EFD} = {469EAEB0-3EDA-4280-8981-6892639E50AE} {925B0D68-397A-41F2-8959-89E7BA8FBAD1} = {469EAEB0-3EDA-4280-8981-6892639E50AE} - {499BFD60-D90F-4307-B5C8-F0A9BE22EA50} = {469EAEB0-3EDA-4280-8981-6892639E50AE} {DB288744-C989-487B-B1F3-525B7085446F} = {469EAEB0-3EDA-4280-8981-6892639E50AE} {E296FB16-8A13-47C7-8604-84418813E699} = {469EAEB0-3EDA-4280-8981-6892639E50AE} + {48E954B7-06B4-44C2-8872-C8F98ADC5665} = {469EAEB0-3EDA-4280-8981-6892639E50AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {554F02C5-5D61-4F2F-AEB0-60790E05B434} diff --git a/AccessTokenClient/AccessTokenClient.csproj b/AccessTokenClient/AccessTokenClient.csproj index cb305d0..c536891 100644 --- a/AccessTokenClient/AccessTokenClient.csproj +++ b/AccessTokenClient/AccessTokenClient.csproj @@ -1,8 +1,9 @@ - + - netstandard2.0;netcoreapp3.1;net5.0 - latest + net6.0 + true + enable 1.0.0 true William Applegate @@ -22,32 +23,18 @@ true - - - - - - - - - - - - - - - - - - - + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/AccessTokenClient/AccessTokenDelegatingHandler.cs b/AccessTokenClient/AccessTokenDelegatingHandler.cs index 4c1dbee..20abeb5 100644 --- a/AccessTokenClient/AccessTokenDelegatingHandler.cs +++ b/AccessTokenClient/AccessTokenDelegatingHandler.cs @@ -1,56 +1,55 @@ -using System; +using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// Delegating handler that automatically requests an access +/// token and adds an authorization header to the outgoing request. +/// +public class AccessTokenDelegatingHandler : DelegatingHandler { + private readonly ITokenRequestOptions options; + + private readonly ITokenClient client; + /// - /// Delegating handler that automatically requests an access - /// token and adds an authorization header to the outgoing request. + /// Initializes a new instance of the class. /// - public class AccessTokenDelegatingHandler : DelegatingHandler + /// The token endpoint options. + /// The access token client. + public AccessTokenDelegatingHandler(ITokenRequestOptions options, ITokenClient client) { - private readonly ITokenRequestOptions options; - - private readonly ITokenClient client; + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } - /// - /// Initializes a new instance of the class. - /// - /// The token endpoint options. - /// The access token client. - public AccessTokenDelegatingHandler(ITokenRequestOptions options, ITokenClient client) + /// + /// Requests an access token and adds an authorization header to the outgoing the request if successful. + /// + /// The http request message. + /// The cancellation token. + /// The . + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var result = await client.RequestAccessToken(new TokenRequest { - this.options = options ?? throw new ArgumentNullException(nameof(options)); - this.client = client ?? throw new ArgumentNullException(nameof(client)); - } + TokenEndpoint = options.TokenEndpoint, + ClientIdentifier = options.ClientIdentifier, + ClientSecret = options.ClientSecret, + Scopes = options.Scopes + }, cancellationToken: cancellationToken); - /// - /// Requests an access token and adds an authorization header to the outgoing the request if successful. - /// - /// The http request message. - /// The cancellation token. - /// The . - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + if (result != null && !string.IsNullOrWhiteSpace(result.AccessToken)) { - var result = await client.RequestAccessToken(new TokenRequest - { - TokenEndpoint = options.TokenEndpoint, - ClientIdentifier = options.ClientIdentifier, - ClientSecret = options.ClientSecret, - Scopes = options.Scopes - }, cancellationToken: cancellationToken); - - if (result != null && !string.IsNullOrWhiteSpace(result.AccessToken)) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); - } - - var response = await base.SendAsync(request, cancellationToken); - - return response; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); } + + var response = await base.SendAsync(request, cancellationToken); + + return response; } } \ No newline at end of file diff --git a/AccessTokenClient/Caching/DefaultAccessTokenTransformer.cs b/AccessTokenClient/Caching/DefaultAccessTokenTransformer.cs index af8d96d..cf7a872 100644 --- a/AccessTokenClient/Caching/DefaultAccessTokenTransformer.cs +++ b/AccessTokenClient/Caching/DefaultAccessTokenTransformer.cs @@ -1,20 +1,19 @@ -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +/// +/// The default transformer which provides no conversion or reversion of the access token. +/// +public class DefaultAccessTokenTransformer : IAccessTokenTransformer { - /// - /// The default transformer which provides no conversion or reversion of the access token. - /// - public class DefaultAccessTokenTransformer : IAccessTokenTransformer + /// + public string Convert(string accessToken) { - /// - public string Convert(string accessToken) - { - return accessToken; - } + return accessToken; + } - /// - public string Revert(string value) - { - return value; - } + /// + public string Revert(string value) + { + return value; } } \ No newline at end of file diff --git a/AccessTokenClient/Caching/IAccessTokenTransformer.cs b/AccessTokenClient/Caching/IAccessTokenTransformer.cs index 17c45b5..460d340 100644 --- a/AccessTokenClient/Caching/IAccessTokenTransformer.cs +++ b/AccessTokenClient/Caching/IAccessTokenTransformer.cs @@ -1,22 +1,21 @@ -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +/// +/// Represents a transformer that converts and reverts an access token from one state to another. +/// +public interface IAccessTokenTransformer { /// - /// Represents a transformer that converts and reverts an access token from one state to another. + /// Converts the access token. /// - public interface IAccessTokenTransformer - { - /// - /// Converts the access token. - /// - /// The access token. - /// The converted access token. - string Convert(string accessToken); + /// The access token. + /// The converted access token. + string Convert(string accessToken); - /// - /// Reverts the access token back to its original state. - /// - /// The converted access token. - /// The access token. - string Revert(string value); - } + /// + /// Reverts the access token back to its original state. + /// + /// The converted access token. + /// The access token. + string Revert(string value); } \ No newline at end of file diff --git a/AccessTokenClient/Caching/IKeyGenerator.cs b/AccessTokenClient/Caching/IKeyGenerator.cs index 4d4a758..b6bbed5 100644 --- a/AccessTokenClient/Caching/IKeyGenerator.cs +++ b/AccessTokenClient/Caching/IKeyGenerator.cs @@ -1,16 +1,15 @@ -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +/// +/// Represents a key generator that generates a cache key from a . +/// +public interface IKeyGenerator { /// - /// Represents a key generator that generates a cache key from a . + /// Generates a cache key from the given token request. /// - public interface IKeyGenerator - { - /// - /// Generates a cache key from the given token request. - /// - /// The token request to generate a key for. - /// The cache key prefix. - /// The token request key. - string GenerateTokenRequestKey(TokenRequest request, string prefix); - } + /// The token request to generate a key for. + /// The cache key prefix. + /// The token request key. + string GenerateTokenRequestKey(TokenRequest request, string prefix); } \ No newline at end of file diff --git a/AccessTokenClient/Caching/ITokenResponseCache.cs b/AccessTokenClient/Caching/ITokenResponseCache.cs index da9e390..cf17bb6 100644 --- a/AccessTokenClient/Caching/ITokenResponseCache.cs +++ b/AccessTokenClient/Caching/ITokenResponseCache.cs @@ -1,30 +1,29 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +/// +/// Represents a token response cache that allows for the storage and retrieval of the . +/// +public interface ITokenResponseCache { /// - /// Represents a token response cache that allows for the storage and retrieval of the . + /// Gets the token response associated to the given key, if it exists. /// - public interface ITokenResponseCache - { - /// - /// Gets the token response associated to the given key, if it exists. - /// - /// The cache key. - /// The cancellation token. - /// A result containing the token response. - Task Get(string key, CancellationToken cancellationToken = default); + /// The cache key. + /// The cancellation token. + /// A result containing the token response. + Task Get(string key, CancellationToken cancellationToken = default); - /// - /// Sets a token response in the cache with the specified expiration. - /// - /// The cache key. - /// The token response to store in the cache. - /// The expiration time span. - /// The cancellation token. - /// A value indicating if the set operation was successful. - Task Set(string key, TokenResponse response, TimeSpan expiration, CancellationToken cancellationToken = default); - } + /// + /// Sets a token response in the cache with the specified expiration. + /// + /// The cache key. + /// The token response to store in the cache. + /// The expiration time span. + /// The cancellation token. + /// A value indicating if the set operation was successful. + Task Set(string key, TokenResponse response, TimeSpan expiration, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/AccessTokenClient/Caching/MemoryTokenResponseCache.cs b/AccessTokenClient/Caching/MemoryTokenResponseCache.cs index 727bb62..607a04f 100644 --- a/AccessTokenClient/Caching/MemoryTokenResponseCache.cs +++ b/AccessTokenClient/Caching/MemoryTokenResponseCache.cs @@ -1,45 +1,44 @@ -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Memory; using System; using System.Threading; using System.Threading.Tasks; -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +/// +/// This class is used to store token responses in a memory +/// cache where they can be retrieved and reused quickly. +/// +public class MemoryTokenResponseCache : ITokenResponseCache { + private readonly IMemoryCache cache; + /// - /// This class is used to store token responses in a memory - /// cache where they can be retrieved and reused quickly. + /// Initializes a new instance of the class. /// - public class MemoryTokenResponseCache : ITokenResponseCache + /// The memory cache. + public MemoryTokenResponseCache(IMemoryCache cache) { - private readonly IMemoryCache cache; - - /// - /// Initializes a new instance of the class. - /// - /// The memory cache. - public MemoryTokenResponseCache(IMemoryCache cache) - { - this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); - } + this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); + } - /// - public Task Get(string key, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); + /// + public Task Get(string key, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); - var exists = cache.TryGetValue(key, out var cachedTokenResponse); + var exists = cache.TryGetValue(key, out var cachedTokenResponse); - return !exists ? Task.FromResult(null) : Task.FromResult(cachedTokenResponse); - } + return !exists ? Task.FromResult(null) : Task.FromResult(cachedTokenResponse); + } - /// - public Task Set(string key, TokenResponse response, TimeSpan expiration, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); + /// + public Task Set(string key, TokenResponse response, TimeSpan expiration, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); - cache.Set(key, response, expiration); + cache.Set(key, response, expiration); - return Task.FromResult(true); - } + return Task.FromResult(true); } } \ No newline at end of file diff --git a/AccessTokenClient/Caching/TokenClientCacheOptions.cs b/AccessTokenClient/Caching/TokenClientCacheOptions.cs index 83fbd61..66badc8 100644 --- a/AccessTokenClient/Caching/TokenClientCacheOptions.cs +++ b/AccessTokenClient/Caching/TokenClientCacheOptions.cs @@ -1,15 +1,14 @@ -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +public class TokenClientCacheOptions { - public class TokenClientCacheOptions - { - /// - /// Gets or sets the token expiration buffer. - /// - public int ExpirationBuffer { get; set; } = 5; + /// + /// Gets or sets the token expiration buffer. + /// + public int ExpirationBuffer { get; set; } = 5; - /// - /// Gets or sets the cache key prefix. - /// - public string CacheKeyPrefix { get; set; } = "AccessTokenClient"; - } + /// + /// Gets or sets the cache key prefix. + /// + public string CacheKeyPrefix { get; set; } = "AccessTokenClient"; } \ No newline at end of file diff --git a/AccessTokenClient/Caching/TokenClientCachingDecorator.cs b/AccessTokenClient/Caching/TokenClientCachingDecorator.cs index fc201bc..a18cf34 100644 --- a/AccessTokenClient/Caching/TokenClientCachingDecorator.cs +++ b/AccessTokenClient/Caching/TokenClientCachingDecorator.cs @@ -1,107 +1,106 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; -namespace AccessTokenClient.Caching -{ - /// - /// This decorator class is responsible for managing the cached access tokens that are available - /// and executing token requests using the decorated client instance to make token requests - /// when a cached value does not exist. - /// - public class TokenClientCachingDecorator : ITokenClient - { - private readonly ILogger logger; +namespace AccessTokenClient.Caching; - private readonly ITokenClient decoratedClient; +/// +/// This decorator class is responsible for managing the cached access tokens that are available +/// and executing token requests using the decorated client instance to make token requests +/// when a cached value does not exist. +/// +public class TokenClientCachingDecorator : ITokenClient +{ + private readonly ILogger logger; - private readonly TokenClientCacheOptions options; + private readonly ITokenClient decoratedClient; - private readonly ITokenResponseCache cache; + private readonly TokenClientCacheOptions options; - private readonly IKeyGenerator keyGenerator; + private readonly ITokenResponseCache cache; - private readonly IAccessTokenTransformer transformer; + private readonly IKeyGenerator keyGenerator; - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The decorated instance. - /// The token client cache options. - /// The token response cache. - /// The key generator. - /// The access token transformer. - public TokenClientCachingDecorator(ILogger logger, ITokenClient decoratedClient, TokenClientCacheOptions options, ITokenResponseCache cache, IKeyGenerator keyGenerator, IAccessTokenTransformer transformer) - { - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.decoratedClient = decoratedClient ?? throw new ArgumentNullException(nameof(decoratedClient)); - this.options = options ?? throw new ArgumentNullException(nameof(options)); - this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); - this.keyGenerator = keyGenerator ?? throw new ArgumentNullException(nameof(keyGenerator)); - this.transformer = transformer ?? throw new ArgumentNullException(nameof(transformer)); - } + private readonly IAccessTokenTransformer transformer; - /// - /// Makes a token request to the specified endpoint and returns the response. - /// - /// The token request. - /// An optional execute function that will override the default request implementation. - /// The cancellation token. - /// The token response. - public async Task RequestAccessToken(TokenRequest request, Func> execute = null, CancellationToken cancellationToken = default) - { - TokenRequestValidator.EnsureRequestIsValid(request); + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The decorated instance. + /// The token client cache options. + /// The token response cache. + /// The key generator. + /// The access token transformer. + public TokenClientCachingDecorator(ILogger logger, ITokenClient decoratedClient, TokenClientCacheOptions options, ITokenResponseCache cache, IKeyGenerator keyGenerator, IAccessTokenTransformer transformer) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.decoratedClient = decoratedClient ?? throw new ArgumentNullException(nameof(decoratedClient)); + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.cache = cache ?? throw new ArgumentNullException(nameof(cache)); + this.keyGenerator = keyGenerator ?? throw new ArgumentNullException(nameof(keyGenerator)); + this.transformer = transformer ?? throw new ArgumentNullException(nameof(transformer)); + } - cancellationToken.ThrowIfCancellationRequested(); + /// + /// Makes a token request to the specified endpoint and returns the response. + /// + /// The token request. + /// An optional execute function that will override the default request implementation. + /// The cancellation token. + /// The token response. + public async Task RequestAccessToken(TokenRequest request, Func>? execute = null, CancellationToken cancellationToken = default) + { + TokenRequestValidator.EnsureRequestIsValid(request); - var key = keyGenerator.GenerateTokenRequestKey(request, options.CacheKeyPrefix); + cancellationToken.ThrowIfCancellationRequested(); - logger.LogInformation("Attempting to retrieve the token response with key '{Key}' from the cache.", key); + var key = keyGenerator.GenerateTokenRequestKey(request, options.CacheKeyPrefix); - var cachedTokenResponse = await cache.Get(key, cancellationToken); + logger.LogDebug("Attempting to retrieve the token response with key '{Key}' from the cache.", key); - if (cachedTokenResponse != null) - { - logger.LogInformation("Successfully retrieved the token response with key '{Key}' from the cache.", key); + var cachedTokenResponse = await cache.Get(key, cancellationToken); - cachedTokenResponse.AccessToken = transformer.Revert(cachedTokenResponse.AccessToken); + if (cachedTokenResponse != null) + { + logger.LogDebug("Successfully retrieved the token response with key '{Key}' from the cache.", key); - return cachedTokenResponse; - } + cachedTokenResponse.AccessToken = transformer.Revert(cachedTokenResponse.AccessToken); - logger.LogInformation("Token response with key '{Key}' does not exist in the cache.", key); + return cachedTokenResponse; + } - var tokenResponse = await decoratedClient.RequestAccessToken(request, execute, cancellationToken); + logger.LogDebug("Token response with key '{Key}' does not exist in the cache.", key); - await CacheTokenResponse(key, tokenResponse, cancellationToken); + var tokenResponse = await decoratedClient.RequestAccessToken(request, execute, cancellationToken); - return tokenResponse; - } + await CacheTokenResponse(key, tokenResponse, cancellationToken); - private async Task CacheTokenResponse(string key, TokenResponse tokenResponse, CancellationToken cancellationToken) - { - logger.LogInformation("Attempting to store token response with key '{Key}' in the cache.", key); + return tokenResponse; + } - var expirationTimeSpan = TimeSpan.FromMinutes(tokenResponse.ExpiresIn / 60 - options.ExpirationBuffer); + private async Task CacheTokenResponse(string key, TokenResponse tokenResponse, CancellationToken cancellationToken) + { + logger.LogDebug("Attempting to store token response with key '{Key}' in the cache.", key); - var accessTokenValue = tokenResponse.AccessToken; + var expirationTimeSpan = TimeSpan.FromMinutes(tokenResponse.ExpiresIn / 60 - options.ExpirationBuffer); - tokenResponse.AccessToken = transformer.Convert(tokenResponse.AccessToken); + var accessTokenValue = tokenResponse.AccessToken; - var tokenStoredSuccessfully = await cache.Set(key, tokenResponse, expirationTimeSpan, cancellationToken); + tokenResponse.AccessToken = transformer.Convert(tokenResponse.AccessToken); - if (tokenStoredSuccessfully) - { - logger.LogInformation("Successfully stored token response with key '{Key}' in the cache.", key); - } - else - { - logger.LogWarning("Unable to store token response with key '{Key}' in the cache.", key); - } + var tokenStoredSuccessfully = await cache.Set(key, tokenResponse, expirationTimeSpan, cancellationToken); - tokenResponse.AccessToken = accessTokenValue; + if (tokenStoredSuccessfully) + { + logger.LogDebug("Successfully stored token response with key '{Key}' in the cache.", key); } + else + { + logger.LogWarning("Unable to store token response with key '{Key}' in the cache.", key); + } + + tokenResponse.AccessToken = accessTokenValue; } } \ No newline at end of file diff --git a/AccessTokenClient/Caching/TokenRequestKeyGenerator.cs b/AccessTokenClient/Caching/TokenRequestKeyGenerator.cs index bbe6fa1..47f899d 100644 --- a/AccessTokenClient/Caching/TokenRequestKeyGenerator.cs +++ b/AccessTokenClient/Caching/TokenRequestKeyGenerator.cs @@ -1,41 +1,40 @@ -using System; +using System; using System.Linq; using System.Security.Cryptography; using System.Text; -namespace AccessTokenClient.Caching +namespace AccessTokenClient.Caching; + +/// +/// A key generator that returns a hash of the . +/// +public class TokenRequestKeyGenerator : IKeyGenerator { - /// - /// A key generator that returns a hash of the . - /// - public class TokenRequestKeyGenerator : IKeyGenerator + /// + public string GenerateTokenRequestKey(TokenRequest request, string prefix) { - /// - public string GenerateTokenRequestKey(TokenRequest request, string prefix) - { - TokenRequestValidator.EnsureRequestIsValid(request); + TokenRequestValidator.EnsureRequestIsValid(request); - var concatenatedRequest = GenerateConcatenatedRequest(request); + var concatenatedRequest = GenerateConcatenatedRequest(request); - using var hasher = new SHA256Managed(); - var textData = Encoding.UTF8.GetBytes(concatenatedRequest); - var hash = hasher.ComputeHash(textData); + using var hasher = SHA256.Create(); + var textData = Encoding.UTF8.GetBytes(concatenatedRequest); + var hash = hasher.ComputeHash(textData); - var convertedHash = BitConverter.ToString(hash).Replace("-", string.Empty); + var convertedHash = BitConverter.ToString(hash).Replace("-", string.Empty); - return $"{prefix}::{convertedHash}"; - } + return $"{prefix}::{convertedHash}"; + } - private static string GenerateConcatenatedRequest(TokenRequest request) - { - var tokenEndpoint = request.TokenEndpoint; - var clientIdentifier = request.ClientIdentifier; - var clientSecret = request.ClientSecret; - var scopes = string.Join(",", request.Scopes.Select(s => s)); + private static string GenerateConcatenatedRequest(TokenRequest request) + { + var tokenEndpoint = request.TokenEndpoint; + var clientIdentifier = request.ClientIdentifier; + var clientSecret = request.ClientSecret; + var scopes = string.Join(",", request.Scopes.Select(s => s)); - var concatenated = $"{tokenEndpoint}:{clientIdentifier}:{clientSecret}:{scopes}"; + var concatenated = $"{tokenEndpoint}:{clientIdentifier}:{clientSecret}:{scopes}"; - return concatenated; - } + return concatenated; } } \ No newline at end of file diff --git a/AccessTokenClient/Extensions/HttpClientExtensions.cs b/AccessTokenClient/Extensions/HttpClientExtensions.cs deleted file mode 100644 index f5b811b..0000000 --- a/AccessTokenClient/Extensions/HttpClientExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace AccessTokenClient.Extensions -{ - /// - /// This static class provides extension methods for the http client - /// in order to make client credentials access token requests. - /// - public static class HttpClientExtensions - { - /// - /// Executes the token request. - /// - /// The http client. - /// The token request. - /// The cancellation token. - /// The token response. - public static async Task ExecuteClientCredentialsTokenRequest(this HttpClient client, TokenRequest request, CancellationToken cancellationToken) - { - TokenRequestValidator.EnsureRequestIsValid(request); - - cancellationToken.ThrowIfCancellationRequested(); - - var uri = new Uri(request.TokenEndpoint); - - var scopes = request.Scopes != null ? string.Join(" ", request.Scopes) : string.Empty; - - var content = new FormUrlEncodedContent(new[] - { - new KeyValuePair("client_id", request.ClientIdentifier), - new KeyValuePair("client_secret", request.ClientSecret), - new KeyValuePair("grant_type", "client_credentials"), - new KeyValuePair("scope", scopes) - }); - - var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content }; - - var responseMessage = await client.SendAsync(requestMessage, cancellationToken); - - if (responseMessage.IsSuccessStatusCode) - { - return await responseMessage.Content.ReadAsStringAsync(); - } - - var statusCode = (int)responseMessage.StatusCode; - - throw new HttpRequestException($"The token request failed with status code {statusCode}."); - } - } -} \ No newline at end of file diff --git a/AccessTokenClient/HttpClientBuilderExtensions.cs b/AccessTokenClient/HttpClientBuilderExtensions.cs index c9ccc75..62d54a2 100644 --- a/AccessTokenClient/HttpClientBuilderExtensions.cs +++ b/AccessTokenClient/HttpClientBuilderExtensions.cs @@ -1,30 +1,29 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// This static class contains an extension method that adds the +/// handler to the instance in order to initiate automatic client credentials +/// token requests for the registered client. +/// +public static class HttpClientBuilderExtensions { /// - /// This static class contains an extension method that adds the - /// handler to the instance in order to initiate automatic client credentials - /// token requests for the registered client. + /// Adds the as a message handler for the named client. /// - public static class HttpClientBuilderExtensions + /// The type of options to inject into the handler. + /// The http client builder. + /// The http client builder instance. + public static IHttpClientBuilder AddClientAccessTokenHandler(this IHttpClientBuilder httpClientBuilder) where T : ITokenRequestOptions { - /// - /// Adds the as a message handler for the named client. - /// - /// The type of options to inject into the handler. - /// The http client builder. - /// The http client builder instance. - public static IHttpClientBuilder AddClientAccessTokenHandler(this IHttpClientBuilder httpClientBuilder) where T : ITokenRequestOptions + return httpClientBuilder.AddHttpMessageHandler(provider => { - return httpClientBuilder.AddHttpMessageHandler(provider => - { - var options = provider.GetRequiredService(); + var options = provider.GetRequiredService(); - var tokenClient = provider.GetRequiredService(); + var tokenClient = provider.GetRequiredService(); - return new AccessTokenDelegatingHandler(options, tokenClient); - }); - } + return new AccessTokenDelegatingHandler(options, tokenClient); + }); } } \ No newline at end of file diff --git a/AccessTokenClient/ITokenClient.cs b/AccessTokenClient/ITokenClient.cs index b036bde..cb3a544 100644 --- a/AccessTokenClient/ITokenClient.cs +++ b/AccessTokenClient/ITokenClient.cs @@ -1,23 +1,22 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// Represents a token client that requests access tokens. +/// +public interface ITokenClient { /// - /// Represents a token client that requests access tokens. + /// Executes a token request to the specified endpoint and returns the token response. /// - public interface ITokenClient - { - /// - /// Executes a token request to the specified endpoint and returns the token response. - /// - /// The token request. - /// - /// An optional function that can be passed in to override the method that executes the token request. - /// - /// The cancellation token. - /// The token response. - Task RequestAccessToken(TokenRequest request, Func> execute = null, CancellationToken cancellationToken = default); - } + /// The token request. + /// + /// An optional function that can be passed in to override the method that executes the token request. + /// + /// The cancellation token. + /// The token response. + Task RequestAccessToken(TokenRequest request, Func>? execute = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/AccessTokenClient/ITokenRequestOptions.cs b/AccessTokenClient/ITokenRequestOptions.cs index dd6600f..850493b 100644 --- a/AccessTokenClient/ITokenRequestOptions.cs +++ b/AccessTokenClient/ITokenRequestOptions.cs @@ -1,29 +1,28 @@ -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// Represents token request options that are necessary +/// to make a client credentials request. +/// +public interface ITokenRequestOptions { /// - /// Represents token request options that are necessary - /// to make a client credentials request. + /// Gets or sets the token endpoint. /// - public interface ITokenRequestOptions - { - /// - /// Gets or sets the token endpoint. - /// - string TokenEndpoint { get; set; } + string TokenEndpoint { get; set; } - /// - /// Gets or sets the client identifier. - /// - string ClientIdentifier { get; set; } + /// + /// Gets or sets the client identifier. + /// + string ClientIdentifier { get; set; } - /// - /// Gets or sets the client secret. - /// - string ClientSecret { get; set; } + /// + /// Gets or sets the client secret. + /// + string ClientSecret { get; set; } - /// - /// Gets or sets the scopes. - /// - string[] Scopes { get; set; } - } + /// + /// Gets or sets the scopes. + /// + string[] Scopes { get; set; } } \ No newline at end of file diff --git a/AccessTokenClient/Serialization/IResponseDeserializer.cs b/AccessTokenClient/Serialization/IResponseDeserializer.cs deleted file mode 100644 index 1775c8d..0000000 --- a/AccessTokenClient/Serialization/IResponseDeserializer.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace AccessTokenClient.Serialization -{ - /// - /// Represents a deserializer that converts a to a . - /// - public interface IResponseDeserializer - { - /// - /// Deserializes the specified string to a . - /// - /// The string content. - /// The token response. - TokenResponse Deserialize(string content); - } -} \ No newline at end of file diff --git a/AccessTokenClient/Serialization/ResponseDeserializer.cs b/AccessTokenClient/Serialization/ResponseDeserializer.cs deleted file mode 100644 index 55d4ae4..0000000 --- a/AccessTokenClient/Serialization/ResponseDeserializer.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json; - -namespace AccessTokenClient.Serialization -{ - /// - /// A response deserializer that converts a json to a . - /// - public class ResponseDeserializer : IResponseDeserializer - { - /// - public TokenResponse Deserialize(string content) - { - return JsonSerializer.Deserialize(content); - } - } -} \ No newline at end of file diff --git a/AccessTokenClient/TokenClient.cs b/AccessTokenClient/TokenClient.cs index bae60ec..18c3863 100644 --- a/AccessTokenClient/TokenClient.cs +++ b/AccessTokenClient/TokenClient.cs @@ -1,95 +1,117 @@ -using AccessTokenClient.Extensions; -using AccessTokenClient.Serialization; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// This class contains a method that requests an access token from a +/// specified token endpoint using the client credentials oauth flow. +/// +public class TokenClient : ITokenClient { + private readonly ILogger logger; + + private readonly HttpClient client; + /// - /// This class contains a method that requests an access token from a - /// specified token endpoint using the client credentials oauth flow. + /// Initializes a new instance of the class. /// - public class TokenClient : ITokenClient + /// The logger. + /// The http client. + public TokenClient(ILogger logger, HttpClient client) { - private readonly ILogger logger; + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } - private readonly HttpClient client; + /// + /// Executes a token request to the specified endpoint and returns the token response. + /// + /// The token request. + /// + /// An optional function that can be passed in to override the method that executes the token request. + /// + /// The cancellation token. + /// The token response. + public async Task RequestAccessToken(TokenRequest request, Func>? execute = null, CancellationToken cancellationToken = default) + { + TokenRequestValidator.EnsureRequestIsValid(request); - private readonly IResponseDeserializer deserializer; + try + { + logger.LogInformation("Executing token request to token endpoint '{TokenEndpoint}'.", request.TokenEndpoint); + + var tokenResponse = await ExecuteTokenRequest(request, execute, cancellationToken); - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The http client. - /// The response deserializer. - public TokenClient(ILogger logger, HttpClient client, IResponseDeserializer deserializer) + return tokenResponse; + } + catch (Exception exception) { - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.client = client ?? throw new ArgumentNullException(nameof(client)); - this.deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); + logger.LogError(exception, "An error occurred when executing the token request to token endpoint '{TokenEndpoint}'.", request.TokenEndpoint); + throw; } + } - /// - /// Executes a token request to the specified endpoint and returns the token response. - /// - /// The token request. - /// - /// An optional function that can be passed in to override the method that executes the token request. - /// - /// The cancellation token. - /// The token response. - public async Task RequestAccessToken(TokenRequest request, Func> execute = null, CancellationToken cancellationToken = default) + private async Task ExecuteTokenRequest(TokenRequest request, Func>? execute, CancellationToken cancellationToken) + { + if (execute != null) { - TokenRequestValidator.EnsureRequestIsValid(request); + return await execute(request); + } - try - { - logger.LogInformation("Executing token request to token endpoint '{TokenEndpoint}'.", request.TokenEndpoint); + TokenRequestValidator.EnsureRequestIsValid(request); - var tokenResponse = await ExecuteTokenRequest(request, execute, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - return tokenResponse; - } - catch (Exception exception) - { - logger.LogError(exception, "An error occurred when executing the token request to token endpoint '{TokenEndpoint}'.", request.TokenEndpoint); - throw; - } - } + var uri = new Uri(request.TokenEndpoint); + + var scopes = request.Scopes != null ? string.Join(" ", request.Scopes) : string.Empty; - private async Task ExecuteTokenRequest(TokenRequest request, Func> execute, CancellationToken cancellationToken) + var content = new FormUrlEncodedContent(new[] { - if (execute != null) - { - return await execute(request); - } + new KeyValuePair("client_id", request.ClientIdentifier), + new KeyValuePair("client_secret", request.ClientSecret), + new KeyValuePair("grant_type", "client_credentials"), + new KeyValuePair("scope", scopes) + }); - var content = await client.ExecuteClientCredentialsTokenRequest(request, cancellationToken); + var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content }; - if (string.IsNullOrWhiteSpace(content)) - { - throw new HttpRequestException($"A null or empty response was returned from token endpoint '{request.TokenEndpoint}'."); - } + var responseMessage = await client.SendAsync(requestMessage, cancellationToken); - var tokenResponse = deserializer.Deserialize(content); + if (!responseMessage.IsSuccessStatusCode) + { + var statusCode = (int)responseMessage.StatusCode; - if (!TokenResponseValid(tokenResponse)) - { - throw new HttpRequestException($"An invalid token response was returned from token endpoint '{request.TokenEndpoint}'."); - } + throw new HttpRequestException($"The token request failed with status code {statusCode}."); + } + + var responseStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken); - logger.LogInformation("Token response from token endpoint '{TokenEndpoint}' is valid.", request.TokenEndpoint); + var tokenResponse = await JsonSerializer.DeserializeAsync(responseStream, TokenResponseJsonContext.Default.TokenResponse, cancellationToken); - return tokenResponse; + if (tokenResponse == null) + { + throw new InvalidOperationException("The token response was not deserialized successfully."); } - private static bool TokenResponseValid(TokenResponse response) + if (!TokenResponseValid(tokenResponse)) { - return response != null && !string.IsNullOrWhiteSpace(response.AccessToken); + throw new HttpRequestException($"An invalid token response was returned from token endpoint '{request.TokenEndpoint}'."); } + + logger.LogDebug("Token response from token endpoint '{TokenEndpoint}' is valid.", request.TokenEndpoint); + + return tokenResponse; + } + + private static bool TokenResponseValid(TokenResponse? response) + { + return response != null && !string.IsNullOrWhiteSpace(response.AccessToken); } } \ No newline at end of file diff --git a/AccessTokenClient/TokenRequest.cs b/AccessTokenClient/TokenRequest.cs index 8f0dfc2..192b3b2 100644 --- a/AccessTokenClient/TokenRequest.cs +++ b/AccessTokenClient/TokenRequest.cs @@ -1,29 +1,30 @@ -namespace AccessTokenClient +using System; + +namespace AccessTokenClient; + +/// +/// This class encapsulates the parameters that are +/// necessary to make a client credentials token request. +/// +public class TokenRequest { /// - /// This class encapsulates the parameters that are - /// necessary to make a client credentials token request. + /// Gets or sets the token endpoint URL. /// - public class TokenRequest - { - /// - /// Gets or sets the token endpoint URL. - /// - public string TokenEndpoint { get; set; } + public string? TokenEndpoint { get; set; } - /// - /// Gets or sets the client identifier. - /// - public string ClientIdentifier { get; set; } + /// + /// Gets or sets the client identifier. + /// + public string? ClientIdentifier { get; set; } - /// - /// Gets or sets the client secret. - /// - public string ClientSecret { get; set; } + /// + /// Gets or sets the client secret. + /// + public string? ClientSecret { get; set; } - /// - /// Gets or sets the requested scopes. - /// - public string[] Scopes { get; set; } - } + /// + /// Gets or sets the requested scopes. + /// + public string[] Scopes { get; set; } = Array.Empty(); } \ No newline at end of file diff --git a/AccessTokenClient/TokenRequestValidator.cs b/AccessTokenClient/TokenRequestValidator.cs index d56d891..1c24d46 100644 --- a/AccessTokenClient/TokenRequestValidator.cs +++ b/AccessTokenClient/TokenRequestValidator.cs @@ -1,34 +1,33 @@ -using System; +using System; -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// This static class contains methods used to validate token requests. +/// +public static class TokenRequestValidator { /// - /// This static class contains methods used to validate token requests. + /// Ensures the token request is valid. If a token endpoint, client + /// identifier, or client secret have not been specified this + /// method will throw an exception. /// - public static class TokenRequestValidator + /// The token request. + public static void EnsureRequestIsValid(TokenRequest request) { - /// - /// Ensures the token request is valid. If a token endpoint, client - /// identifier, or client secret have not been specified this - /// method will throw an exception. - /// - /// The token request. - public static void EnsureRequestIsValid(TokenRequest request) + if (string.IsNullOrWhiteSpace(request.TokenEndpoint)) { - if (string.IsNullOrWhiteSpace(request.TokenEndpoint)) - { - throw new ArgumentNullException(nameof(request.TokenEndpoint)); - } + throw new ArgumentException("A token endpoint must be specified."); + } - if (string.IsNullOrWhiteSpace(request.ClientIdentifier)) - { - throw new ArgumentNullException(nameof(request.ClientIdentifier)); - } + if (string.IsNullOrWhiteSpace(request.ClientIdentifier)) + { + throw new ArgumentException("A client identifier must be specified."); + } - if (string.IsNullOrWhiteSpace(request.ClientSecret)) - { - throw new ArgumentNullException(nameof(request.ClientSecret)); - } + if (string.IsNullOrWhiteSpace(request.ClientSecret)) + { + throw new ArgumentException("A client secret must be specified."); } } } \ No newline at end of file diff --git a/AccessTokenClient/TokenResponse.cs b/AccessTokenClient/TokenResponse.cs index 43a3847..7dec62e 100644 --- a/AccessTokenClient/TokenResponse.cs +++ b/AccessTokenClient/TokenResponse.cs @@ -1,22 +1,21 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; -namespace AccessTokenClient +namespace AccessTokenClient; + +/// +/// A class that represents the response from a token endpoint. +/// +public class TokenResponse { /// - /// A class that represents the response from a token endpoint. + /// Gets or sets the access token. /// - public class TokenResponse - { - /// - /// Gets or sets the access token. - /// - [JsonPropertyName("access_token")] - public string AccessToken { get; set; } + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } - /// - /// Gets or sets the expiration for the token in seconds. - /// - [JsonPropertyName("expires_in")] - public int ExpiresIn { get; set; } - } + /// + /// Gets or sets the expiration for the token in seconds. + /// + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } } \ No newline at end of file diff --git a/AccessTokenClient/TokenResponseJsonContext.cs b/AccessTokenClient/TokenResponseJsonContext.cs new file mode 100644 index 0000000..8ed2c3c --- /dev/null +++ b/AccessTokenClient/TokenResponseJsonContext.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace AccessTokenClient; + +[JsonSerializable(typeof(TokenResponse))] +public partial class TokenResponseJsonContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Projects/Benchmarks/Benchmarks.csproj b/Projects/Benchmarks/Benchmarks.csproj index f8c2729..817b8ca 100644 --- a/Projects/Benchmarks/Benchmarks.csproj +++ b/Projects/Benchmarks/Benchmarks.csproj @@ -2,13 +2,23 @@ Exe - net5.0 + net6.0 + enable + enable - + + + + - + + + + + + @@ -16,4 +26,4 @@ - + \ No newline at end of file diff --git a/Projects/Benchmarks/MockHttpMessageHandler.cs b/Projects/Benchmarks/MockHttpMessageHandler.cs new file mode 100644 index 0000000..426b7ce --- /dev/null +++ b/Projects/Benchmarks/MockHttpMessageHandler.cs @@ -0,0 +1,41 @@ +using System.Net; + +namespace Benchmarks; + +public class MockHttpMessageHandler : DelegatingHandler +{ + private readonly HttpStatusCode httpStatusCode; + + private readonly string? response; + + /// + /// Initializes a new instance of the class. + /// + /// The response mock response to use. + /// The HTTP status code to return. + public MockHttpMessageHandler(string? response, HttpStatusCode httpStatusCode) + { + this.response = response; + this.httpStatusCode = httpStatusCode; + } + + public string? Input { get; private set; } + + public int NumberOfCalls { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + NumberOfCalls++; + + if (request.Content != null) + { + Input = await request.Content.ReadAsStringAsync(cancellationToken); + } + + return new HttpResponseMessage + { + StatusCode = httpStatusCode, + Content = response != null ? new StringContent(response) : null + }; + } +} \ No newline at end of file diff --git a/Projects/Benchmarks/Program.cs b/Projects/Benchmarks/Program.cs index 4f7a32c..ec26add 100644 --- a/Projects/Benchmarks/Program.cs +++ b/Projects/Benchmarks/Program.cs @@ -1,49 +1,67 @@ -using AccessTokenClient; +using System.Net; +using AccessTokenClient; using AccessTokenClient.Caching; using AccessTokenClient.Extensions; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using Microsoft.Extensions.DependencyInjection; -using System.Threading.Tasks; +using Microsoft.Extensions.Logging; -namespace Benchmarks +namespace Benchmarks; + +public class Program { - public class Program + [MemoryDiagnoser] + [SimpleJob(RuntimeMoniker.Net60, baseline: true)] + public class AccessTokenClientBenchmarks { - public class AccessTokenClientBenchmarks + private readonly ITokenClient client; + + public AccessTokenClientBenchmarks() { - private readonly ITokenClient client; + const string Response = "{\"access_token\":\"1234567890\",\"token_type\":\"Bearer\",\"expires_in\":7199}"; - public AccessTokenClientBenchmarks() - { - var services = new ServiceCollection(); + var mockHandler = new MockHttpMessageHandler(Response, HttpStatusCode.OK); - services.AddMemoryCache().AddAccessTokenClient().AddAccessTokenClientCache(); + var services = new ServiceCollection() + .AddLogging() + .AddMemoryCache() + .AddAccessTokenClient(builder => + { + // Add the default retry policy: + builder.AddPolicyHandler((provider, _) => + { + var logger = provider.GetRequiredService>(); + return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); + }); - var provider = services.BuildServiceProvider(); + // Set-up a delegating handler to mock the response for the test: + builder.AddHttpMessageHandler(() => mockHandler); + }) + .AddAccessTokenClientCache(); - client = provider.GetService(); - } + client = services.BuildServiceProvider().GetRequiredService(); + } - [Benchmark(Baseline = true)] - public async Task GetAccessToken() + [Benchmark] + public async Task GetAccessToken() + { + return await client.RequestAccessToken(new TokenRequest { - return await client.RequestAccessToken(new TokenRequest + TokenEndpoint = "https://localhost:44303/connect/token", + ClientIdentifier = "testing_client_identifier", + ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", + Scopes = new[] { - TokenEndpoint = "https://localhost:44303/connect/token", - ClientIdentifier = "testing_client_identifier", - ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", - Scopes = new[] - { - "employee:read", "employee:create", "employee:edit", "employee:delete" - } - }); - } + "employee:read", "employee:create", "employee:edit", "employee:delete" + } + }); } + } - public static void Main() - { - var summary = BenchmarkRunner.Run(); - } + public static void Main() + { + var summary = BenchmarkRunner.Run(); } } \ No newline at end of file diff --git a/Projects/Console/Console.csproj b/Projects/Console/Console.csproj index fa81cff..f448147 100644 --- a/Projects/Console/Console.csproj +++ b/Projects/Console/Console.csproj @@ -2,14 +2,14 @@ Exe - net5.0 + net6.0 - - - - + + + + diff --git a/Projects/Console/Program.cs b/Projects/Console/Program.cs index 09c75bb..abeecc9 100644 --- a/Projects/Console/Program.cs +++ b/Projects/Console/Program.cs @@ -1,4 +1,4 @@ -using AccessTokenClient; +using AccessTokenClient; using AccessTokenClient.Caching; using AccessTokenClient.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -7,58 +7,49 @@ using System; using System.Threading.Tasks; -namespace Console -{ - public class Program - { - public static async Task Main() - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Seq("http://localhost:5341") + .CreateLogger(); - var services = new ServiceCollection(); +var services = new ServiceCollection(); - services - .AddLogging(configure => configure.AddSerilog()) - .AddMemoryCache() - .AddAccessTokenClient(builder => builder.AddPolicyHandler(AccessTokenClientPolicy.GetDefaultRetryPolicy())) - .AddAccessTokenClientCache(); +services + .AddLogging(configure => configure.AddSerilog()) + .AddMemoryCache() + .AddAccessTokenClient(builder => builder.AddPolicyHandler(AccessTokenClientPolicy.GetDefaultRetryPolicy())) + .AddAccessTokenClientCache(); - var provider = services.BuildServiceProvider(); +var provider = services.BuildServiceProvider(); - var client = provider.GetService(); +var client = provider.GetRequiredService(); - if (client != null) +if (client != null) +{ + while (true) + { + try + { + var tokenResponse = await client.RequestAccessToken(new TokenRequest { - while (true) + TokenEndpoint = "https://localhost:44303/connect/token", + ClientIdentifier = "testing_client_identifier", + ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", + Scopes = new[] { - try - { - var tokenResponse = await client.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "https://localhost:44303/connect/token", - ClientIdentifier = "testing_client_identifier", - ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", - Scopes = new[] - { - "employee:read", "employee:create", "employee:edit", "employee:delete" - } - }); + "employee:read", "employee:create", "employee:edit", "employee:delete" + } + }); - System.Console.WriteLine(tokenResponse.AccessToken); + Console.WriteLine(tokenResponse.AccessToken); - await Task.Delay(60000); - } - catch (Exception exception) - { - System.Console.WriteLine(exception.Message); - System.Console.WriteLine("An error occurred when requesting an access token."); - } - } - } + await Task.Delay(60000); + } + catch (Exception exception) + { + Console.WriteLine(exception.Message); + Console.WriteLine("An error occurred when requesting an access token."); } } } \ No newline at end of file diff --git a/Projects/IdentityServer/Controllers/HomeController.cs b/Projects/IdentityServer/Controllers/HomeController.cs deleted file mode 100644 index 20fd52e..0000000 --- a/Projects/IdentityServer/Controllers/HomeController.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace IdentityServer.Controllers -{ - [AllowAnonymous] - public class HomeController : Controller - { - public IActionResult Index() - { - return View(); - } - } -} \ No newline at end of file diff --git a/Projects/IdentityServer/IdentityServer.csproj b/Projects/IdentityServer/IdentityServer.csproj index ea633a3..7026422 100644 --- a/Projects/IdentityServer/IdentityServer.csproj +++ b/Projects/IdentityServer/IdentityServer.csproj @@ -1,14 +1,14 @@ - + - netcoreapp3.1 + net6.0 - - - - + + + \ No newline at end of file diff --git a/Projects/IdentityServer/IdentityServerConfiguration.cs b/Projects/IdentityServer/IdentityServerConfiguration.cs index 8341d59..c57f65c 100644 --- a/Projects/IdentityServer/IdentityServerConfiguration.cs +++ b/Projects/IdentityServer/IdentityServerConfiguration.cs @@ -1,34 +1,36 @@ -using IdentityServer4.Models; +using Duende.IdentityServer.Models; using System.Collections.Generic; -namespace IdentityServer +namespace IdentityServer; + +public static class IdentityServerConfiguration { - public static class IdentityServerConfiguration + public static IEnumerable Scopes => new[] { - public static IEnumerable Resources => new[] - { - new ApiResource("employee:read", "The ability to read employee data."), - new ApiResource("employee:create", "The ability create employee data."), - new ApiResource("employee:edit", "The ability to edit employee data."), - new ApiResource("employee:delete", "The ability to delete employee data.") - }; + new ApiScope("employee:read", "The ability to read employee data."), + new ApiScope("employee:create", "The ability create employee data."), + new ApiScope("employee:edit", "The ability to edit employee data."), + new ApiScope("employee:delete", "The ability to delete employee data.") + }; - public static IEnumerable Clients => new[] + public static IEnumerable Clients => new[] + { + new Client { - new Client + ClientId = "testing_client_identifier", + ClientName = "A testing client credentials client", + AllowedGrantTypes = GrantTypes.ClientCredentials, + ClientSecrets = + { + new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) + }, + AllowedScopes = { - ClientId = "testing_client_identifier", - ClientName = "A testing client credentials client", - AllowedGrantTypes = GrantTypes.ClientCredentials, - ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) }, - AllowedScopes = - { - "employee:read", - "employee:create", - "employee:edit", - "employee:delete" - } + "employee:read", + "employee:create", + "employee:edit", + "employee:delete" } - }; - } + } + }; } \ No newline at end of file diff --git a/Projects/IdentityServer/Program.cs b/Projects/IdentityServer/Program.cs index 0d4b33d..8e63303 100644 --- a/Projects/IdentityServer/Program.cs +++ b/Projects/IdentityServer/Program.cs @@ -1,44 +1,43 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Sinks.SystemConsole.Themes; using System; -namespace IdentityServer +namespace IdentityServer; + +public class Program { - public class Program + public static int Main(string[] args) { - public static int Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .Enrich.FromLogContext() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) - .CreateLogger(); + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) + .CreateLogger(); - try - { - Log.Information("Starting the host..."); - CreateHostBuilder(args).Build().Run(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "The host terminated unexpectedly."); - return 1; - } - finally - { - Log.CloseAndFlush(); - } + try + { + Log.Information("Starting the host..."); + CreateHostBuilder(args).Build().Run(); + return 0; } - - public static IHostBuilder CreateHostBuilder(string[] args) + catch (Exception ex) + { + Log.Fatal(ex, "The host terminated unexpectedly."); + return 1; + } + finally { - return Host.CreateDefaultBuilder(args).UseSerilog().ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + Log.CloseAndFlush(); } } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).UseSerilog().ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } } \ No newline at end of file diff --git a/Projects/IdentityServer/Startup.cs b/Projects/IdentityServer/Startup.cs index f61905b..8c612a5 100644 --- a/Projects/IdentityServer/Startup.cs +++ b/Projects/IdentityServer/Startup.cs @@ -1,62 +1,47 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace IdentityServer +namespace IdentityServer; + +public class Startup { - public class Startup + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) + var builder = services.AddIdentityServer(options => { - services.AddControllersWithViews(); - - services.Configure(iis => - { - iis.AuthenticationDisplayName = "Windows"; - iis.AutomaticAuthentication = false; - }); - - services.Configure(iis => - { - iis.AuthenticationDisplayName = "Windows"; - iis.AutomaticAuthentication = false; - }); - - var builder = services.AddIdentityServer(options => - { - options.Events.RaiseErrorEvents = true; - options.Events.RaiseInformationEvents = true; - options.Events.RaiseFailureEvents = true; - options.Events.RaiseSuccessEvents = true; - }) - .AddInMemoryApiResources(IdentityServerConfiguration.Resources) - .AddInMemoryClients(IdentityServerConfiguration.Clients); - - builder.AddDeveloperSigningCredential(); - - services.AddAuthentication(); - } + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + }) + .AddInMemoryApiScopes(IdentityServerConfiguration.Scopes) + .AddInMemoryClients(IdentityServerConfiguration.Clients); + + builder.AddDeveloperSigningCredential(); + + //services.AddAuthentication(); + } - public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + if (environment.IsDevelopment()) { - if (environment.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseDeveloperExceptionPage(); + } - app.UseStaticFiles(); + //app.UseStaticFiles(); - app.UseRouting(); + //app.UseRouting(); - app.UseIdentityServer(); + app.UseIdentityServer(); - app.UseAuthorization(); + //app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapDefaultControllerRoute(); - }); - } + //app.UseEndpoints(endpoints => + //{ + // endpoints.MapDefaultControllerRoute(); + //}); } } \ No newline at end of file diff --git a/Projects/IdentityServer/Views/Home/Index.cshtml b/Projects/IdentityServer/Views/Home/Index.cshtml deleted file mode 100644 index 1701843..0000000 --- a/Projects/IdentityServer/Views/Home/Index.cshtml +++ /dev/null @@ -1,15 +0,0 @@ -
-
-

- - Welcome to IdentityServer4 -

-
-
-

- IdentityServer publishes a - discovery document - where you can find metadata and links to all the endpoints, key material, etc. -

-
-
\ No newline at end of file diff --git a/Projects/IdentityServer/Views/Shared/_Layout.cshtml b/Projects/IdentityServer/Views/Shared/_Layout.cshtml deleted file mode 100644 index 4d3bad1..0000000 --- a/Projects/IdentityServer/Views/Shared/_Layout.cshtml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - IdentityServer4 - - - - -
- @RenderBody() -
- - \ No newline at end of file diff --git a/Projects/IdentityServer/Views/_ViewStart.cshtml b/Projects/IdentityServer/Views/_ViewStart.cshtml deleted file mode 100644 index 1af6e49..0000000 --- a/Projects/IdentityServer/Views/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "_Layout"; -} \ No newline at end of file diff --git a/Projects/IdentityServer/keys/is-signing-key-ADA8BCEA989EF87260028B66E6F0750B.json b/Projects/IdentityServer/keys/is-signing-key-ADA8BCEA989EF87260028B66E6F0750B.json new file mode 100644 index 0000000..4ec4dd7 --- /dev/null +++ b/Projects/IdentityServer/keys/is-signing-key-ADA8BCEA989EF87260028B66E6F0750B.json @@ -0,0 +1 @@ +{"Version":1,"Id":"ADA8BCEA989EF87260028B66E6F0750B","Created":"2022-05-09T02:51:10.5391063Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8BoB9cM8xjdBk48uSDYU7wTcR-8lV5OwbHIYjyBNMjTyLsJUK_dTI6Ga0RJcf-8n3EgT8eI0EuMApO3kmECQoeHP5gN89trWtlJ9wnBTwNHKvL_4p1GEtju9kVrZ_sTIVOLRhxSU_VwQA_3FeYkk53pdjz3Eyezo9k6njXPAPlSSBo5--HO6Prg8JcmTD6uRh60Dat65-Oi0v_m4xLB5PkpzgmCTwsMz3ATPmuIxsHFacelajIv5rcdRSAF9tFeQhkLMA9tpBX1EaOR55ubrW_68LC55FxpwbFjC3GO2BV-omW4GitaXvPFGjKc3LmdgYiG3qFkm1l5o_eZ6zSAxzExNzTVyTgFBwVeZOddILV_3VcmGXjRHpicFoiqDnaDzvSgVTqqCHD8YAW80DyMqHJBGHpJrIcPQdrRXOebX-bBuHMiYXXrcQpiNbkgVboKbxCUI7Zu_c_N6BkdRKj1yb7zWxSgVtNINJkDsxDdUVENh7n4lDK-SNKCgKVwlxfUSlZJh2Qc5LvAdO9-lhp5-m-jPfxvkOr6QdDrAFAD32ZHQbKRMHjxQp5mQL2bCzxD7iuPc93l_OyaEtGtYdyJXOiAqhWo5R6vNb-gByq2NDNLvNCWBbRdvOQorM3N4kiVRvRgVASQu45WYfQfjD901v1MJSnDMkF_Wpxz4elVxjH-TlsX07jnlZInF37iTg3A71EpNzep9HKpxyw5g4Kz0XINjyeRKtDdm1kd9WNbNsQCGOrpdHP5G1v4CZ9ZkdL8XLE4vkG713T8nId5T1JSKa7Og8KlJcGc9MD1FWXOieYttiCsklPHz0KkdXS4vwZgqgX3sW0rHrn_tLlcuehUmYTyt59mMIDsrgnijHj1yMOpDW1g9Ms2EFD7_tg1CYDwg3w-YIVoUBZvtKz24EoezCKfa99CS7CgFfJgmqoG7PS3cNapkmgy6UAh0p11JgRhQmvSFJR5rZ48Lznnf_1WErgMRPiaFIWszvmpgut3KFzUoaNPnPhAIouRfOYvsRuj_7w_ys6A_cMrDqrVf9b7kxayMVPW54XCfOTwf5llLS-ROGA4IEBl0npI-H-d81sDqVED1Xc1R8OduZNQgu5dNlBJ_vMxMOsvJp6Vc8Ikd8W8Tt3vgKuTMfGo2d_-sdSCId-aWWODhXw7TBgCvw-htPhZ4vo2SvZ9AZk7LWS6TbUB2XZL8lO_CATninhCHVSklo_y7r0kFYX282ZKkV5OEwzMFOJDMcPXvrABliWA7gjWOdtnhK2MfBF5gvW4hX88YZ5-IWM1RjfRIxbYgQuEhn6G8Tk8A0nCISLIYPogT_xI4rc1nh8FEy5ux3gYD8Nby_071ukAu2T-BPqUxpfFYW_28wsZjF4doBil4c4hqDWoe0-yFXYyvRZhrOG9ZAC9yxAN0d_qkvT4I3Im1QAVFpX_SDcxk7dveGPrlDETji5NraLI3SSQ4EgjHbVZ0QcXSkNj4AFhY-lToU5SIN0Ltr22enmAXQV03LUvgz4Mz_AROmxL4-IknxSbpHzAuhybC6ib7kw4fh2t2zEY79FFsFmLmp6dgtOVIQO8Rhofg41OWP4WTcpktKROnh1JAFOirieFUeVpIX4Cy4TJcFXyc5i2aDsgrZ2Y6nUvfxCU1XzpugZyH8O08QjkKCJBINvYyJDZ_nludS-aTNXpSr8twF6p2pjpMCqN_MQ9LbOYK5lwacLJgQs4NtfZshwJs82KfnPgM_mytkdv78YrK3Vq-rVeoLW2QErSqyvLtetqEKIlPF45rNDKUbhg9VdZSz9rc2k6JV-_OXZr_X7XEanz8niGVBaz2ihlJtK5fkQrK1Wkbl-z6FkfP4klurqkmOQD9EU4caGqlmTQQdJ9hy2PR5aJXazytLObpOyVRFHnUVxDfIRRCGmcq7NDNs8CIOb6v2gBgqM7mCuZz5tZj6cOozaITCTkTj5q98NHJ3Zu0hI8aGVdNulAuxGHj-wj7_ZhVXhKy2CO60-ZYlHg3FPH67AMnzN2YaX9e4Y1BH5urBxc2jIak1M9FP_ba7dTsPKVZXfEL7GR-K5HPQNU32SLA2CeDqFisvxADCj383gOTpM2-HIRzcsrHrasiMNa8a0p5be3Xlj25okQy_lLOQatRXQNjPN5mDXbjeDqxTk-77FfHrsDEuHF2e6Gw8pzYhRVb2o349H-fVVzfRh8qjD_U-zXM5wMzWV1QQIU-W-vUcnwk1_wSBIe0cLyw1tacTPyL6PkGDwHZddNVSdKkobPuPyLc1nwqh7D_qNBuGRWDT66osZluKkVL2CLT3m4SDVOBN1sIV-C9uONBmWLN1vArHyaTkVwb2j7FzU6eQ_7IyjLiKJEmePGCfK3VNwpJeXSW0D2JoD77HNaaZFOTv5ohL06G-VQDT6UtwfbYk1zyIXcM4uz_P6xxtaVmy-aYpYuCw1g4UPXvJzJbJUZqb16aurruhhi7kO5Yi3ObYPoRO_ab","DataProtected":true} \ No newline at end of file diff --git a/Projects/IdentityServer/tempkey.jwk b/Projects/IdentityServer/tempkey.jwk new file mode 100644 index 0000000..4a8bb2f --- /dev/null +++ b/Projects/IdentityServer/tempkey.jwk @@ -0,0 +1 @@ +{"AdditionalData":{},"Alg":"RS256","Crv":null,"D":"Wnp0tExdfyLkRiN_L677PAsNRsHdtantKKB5fHB_3PvC-29ILvx3vGNnrwyzx18rTIuuqyet0EZv0_LlADjyFX2Wmh8oLUr9DcdZSTjMG6gwje_Vg9G-hJD7-jip2roMlv7maaIktwBRn95P7MiJZ88K8KmX368nrzc6R77qiBE-rcQhmu31nn9QKz8EKnfS6Mw7JH6JpIYkIiDnn3JSLirmyxtoV_ECrokSBSzPsn12jh34tBDKzU13M-Sgs7dzPi9le2Ri_u8GqC_fu-j3vaehU32Vr87hQA99Dj2--PoeJoaW833GKe68plM0l1gydSjmQ5hR2ygjzRccKfsLOQ","DP":"aivSmJCS0g-dRqIOJPjc327UR1gXRnri314qwqGYmxEV9o0mqejCPeHsb0LRIqfUN9tMLoeDloVLtrRDkuLVnGb4VsNOFNyK39gamYQwj4swBNahfFt0c8kdZvcb8LXjSORbRNb3IPYYH4C_qIr4iaOcuszxPuBVx06-IfhjMUc","DQ":"Nq8VhNnw1NuUMNjt8Z2VO79KtoGcXTIKuDWzFPmL6SU9b0aAnRWWGX1fI57W0VRLuX5r6OecOhRVFWhLGiMZip9h-QagMr6PMIzi8kh2fuGmSWzI9PQ0LHm0S6d1lDzNnguU_CJkM8V9ZXr9Gpz7oDHHPSFqCyhqtE01iQ5xD8c","E":"AQAB","K":null,"KeyId":"8D33060FA718E89F64388A0BC077F64B","KeyOps":[],"Kid":"8D33060FA718E89F64388A0BC077F64B","Kty":"RSA","N":"5Cb_aptKy6lGegTR398DASU54FqBgJ3xK-NCTJLEYg_04aAY4jNTR6PYOvw5zNlG0XqBft7k33BFSc2JysAs5nxEjYB8T4o3whtfpggM9407BahemG4vTW9ylh6TMQg1R14exsb98Vp83BYkAKKLrPMfp6Gupu1gnKSMVPUq9JlUGfCJuqVomPQGkrwGak-6CvkCzXFPhswgFYZsieVh_QfZEjfJB9bo6aaykQq4QuQp3M8UnRw3Doq20yaUs6tEJqGCEg2YV0b1gGxnZKeDRT8iiSWpVbv1WjQdSYACePCRAqDjz2A3wmmTKHd3hZc9bhfAKTHJw7GHFSxRkyN5mQ","Oth":null,"P":"88Kp6rOqUpV-29fB2-J-L8MxPxhj7gwC_2iQAIqSPjOKgxcxpiaC3wHSyt_DNUoPHiNadi508mcsYlQEef2MEaeiEE0ZWthwQCr_fENH5XzKN6UncUTf8xBXiAJADMmJFl9n-u0RUS7Z492EspB9KsDSWJqT9RKmpUpVS32ltEc","Q":"75u0jYDtaKty-UIqdNLqIbobcz7qSz9pjpksdgp0rDQ2gVwQSX-VdLP_8tINQB4SwVeApb2Tex9-6KBZUTe0yem5utQVuegsfFR5gepYRY-R08qSO3rb4VxaQeQw4Jljgh4BH_HD9fcEYZD2j_qhN1Mwm2wJmDfUQ7VWoJRTsx8","QI":"Q5LcbAmbfQ3RZKI8kaNZsn-ST2CYBqsuVK75G1mVgY6e5ndwIh_Vbulqzf0pSUPBwO4UDwvj0UMacMIIA45--FCvxAPmaEYis_UXar4hYWB03rayKuoYoVgC33nS9Z7iVFo0fkT6wUc-gbgq3fBFLFyof7STTc0Uy3WyW5had1I","Use":null,"X":null,"X5c":[],"X5t":null,"X5tS256":null,"X5u":null,"Y":null,"KeySize":2048,"HasPrivateKey":true,"CryptoProviderFactory":{"CryptoProviderCache":{},"CustomCryptoProvider":null,"CacheSignatureProviders":true,"SignatureProviderObjectPoolCacheSize":128}} \ No newline at end of file diff --git a/Projects/IdentityServer/tempkey.rsa b/Projects/IdentityServer/tempkey.rsa deleted file mode 100644 index 55bd6e3..0000000 --- a/Projects/IdentityServer/tempkey.rsa +++ /dev/null @@ -1 +0,0 @@ -{"KeyId":"iJcdSpbIk6iIRZvQTg_hHg","Parameters":{"D":"bh8ZVKxIqhWPLqo5oPhury9sT8AInakRjM8GqAfYU3rD1DpEasSk48aKvAK6lYCtOI1t6qeJmgMU3fvW7CtNxXUzPR9+StArBHEdV2KibTMfs7Mm7KQ2hBgtfhEOr1ZqdVzvJZim3MF5vbYHFyxVs98q1xyhNwigu96EFIanaR7+YqilRHLY+YAnJnX8cMLbbBN4VlhRVDODfqXZU8QH819AqMAm8+qCXRQtoxq+osYGP/T+ScdeYeYPikDM8Wmy+d8VxrXVhAOKHqUy8NC1lOxVC2oaOhNik85vwtRRCJGgsRXU1Z7JToCIuNAqNKthdHvKRbRIyxJt8mjuSSu0xQ==","DP":"djSw+kyjZ3J0uRGN8cfExQwUqBAhJ7gMdSLO5jg9ur2jfdrhgwVsp+GiYuGZzG6k/ggasVS2nRnZ90zRJPeoxwbBiTSN2s9Ne64gQeziQyxw/WENksFGbkpu+4aSg9k42kZd57Bfia2NpXtfJdNmHSUbXMO2TmhmCASADtCtdAU=","DQ":"DzRlQT8uk/Njz+9/RldKAI+dEKafx1InUOn+5DLJ26NxnhPEf+V+hsiMrAcv8m+0E86fiLNc2hQUZsRW3GOXm4RUrYb75Iu5GrnhVLyu11iPocfBNhDyIaRfZxkdmBBSrCUhZ67Dmfw9S4Rq9qPoCNIGEy9bY1i0YMDR2TNU2n8=","Exponent":"AQAB","InverseQ":"MvthVUgH7+TESxjx22MbuW6Pgv3ag1iJdTnFgPTcbjpqXRmBPuNP7UXkmWi7E6XT9RSZpalr1KwJcQ5PrN9JazBMD58DrXoBsgyneUdzgxMYl4Q/SOcawbFEeitMNx0VmyRkI5nNh4AThTwoQ89cW1GeW17DLVTgWETSo/bUI3o=","Modulus":"3E6MbWrsMVfqtvOtuJsaNxQWZ+v/U1c6jd8gnmgkCS7T6OKROEgUYkXF7QWXlR8BoJKXPs1S8u5yhi7ZG0he+DNlAb+WTS5Y63jSD8UPesG7bNNcHBA/dROejaMO99egwNxYg0jUCblcBtgEyr0bDFCbF3aW/fGKhLvVGxFfGD5eQys7TvfFHnPsKRKvrCTd5tKE7Z2ClwbdrewcuRhzdxRSevs0FxuHJXCjeHfCwF9N+VVCh5hzDVC4bbfml7fu9DBkuVxffKBtl/DItJzc7xbfZdNOSwDjNab8Qz8g8LBAe7HnDZtG5e1VXVLyJuXtRLp+PYic6SQ34FC8HbsCbQ==","P":"8Aiv5NJXMKuOcMfRMgiF/sTzhBtsRY7bskKAut9+cfaQCubRjIg9Jp2c0kN1tlywktDPUwMwMaB1Z2iozxDAIuW/0DO1r+1IWS9q4JUHiTLozh+X1ZoQ+KFJAa2WA84pDW5F8g6/XquMuSyxwGen7S3NyfWV8vMK3sjfZMp16HM=","Q":"6vXydQKVxNeaTsrkpU7qUhIWG952ypMVQHyT5r+3eCQNXyfQaeBlL3MzSHzUVbHM5zuoy3pUO7HrSUPi1AXLbiiyTdkXVQdIZwn2nuBnlbmODhaD0n7lLISe2AKyoBP22mHC10PdhHzJmR+4O1Q/Te6rYi0vq0F6M90lfJmwEZ8="}} \ No newline at end of file diff --git a/Projects/IdentityServer/wwwroot/favicon.ico b/Projects/IdentityServer/wwwroot/favicon.ico deleted file mode 100644 index ee470e4..0000000 Binary files a/Projects/IdentityServer/wwwroot/favicon.ico and /dev/null differ diff --git a/Projects/IdentityServer/wwwroot/icon.jpg b/Projects/IdentityServer/wwwroot/icon.jpg deleted file mode 100644 index e652502..0000000 Binary files a/Projects/IdentityServer/wwwroot/icon.jpg and /dev/null differ diff --git a/Projects/IdentityServer/wwwroot/icon.png b/Projects/IdentityServer/wwwroot/icon.png deleted file mode 100644 index cd386d5..0000000 Binary files a/Projects/IdentityServer/wwwroot/icon.png and /dev/null differ diff --git a/Projects/TestingFivePointZeroApplication/Controllers/ServiceController.cs b/Projects/TestingFivePointZeroApplication/Controllers/ServiceController.cs deleted file mode 100644 index 9693a9b..0000000 --- a/Projects/TestingFivePointZeroApplication/Controllers/ServiceController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using AccessTokenClient; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; - -namespace TestingFivePointZeroApplication.Controllers -{ - [ApiController] - [Route("service")] - public class ServiceController : ControllerBase - { - private readonly ITokenClient client; - - public ServiceController(ITokenClient client) - { - this.client = client; - } - - [HttpGet] - public async Task Get() - { - var tokenResponse = await client.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "https://localhost:44303/connect/token", - ClientIdentifier = "testing_client_identifier", - ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", - Scopes = new[] - { - "employee:read", "employee:create", "employee:edit", "employee:delete" - } - }); - - return Ok(tokenResponse); - } - } -} \ No newline at end of file diff --git a/Projects/TestingFivePointZeroApplication/Program.cs b/Projects/TestingFivePointZeroApplication/Program.cs deleted file mode 100644 index e8bc319..0000000 --- a/Projects/TestingFivePointZeroApplication/Program.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Serilog; -using Serilog.Events; - -namespace TestingFivePointZeroApplication -{ - public class Program - { - public static void Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); - - try - { - Log.Information("Starting the host..."); - CreateHostBuilder(args).Build().Run(); - } - catch (Exception ex) - { - Log.Fatal(ex, "The host terminated unexpectedly"); - } - finally - { - Log.CloseAndFlush(); - } - } - - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args).UseSerilog().ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } - } -} \ No newline at end of file diff --git a/Projects/TestingFivePointZeroApplication/Startup.cs b/Projects/TestingFivePointZeroApplication/Startup.cs deleted file mode 100644 index 55201a9..0000000 --- a/Projects/TestingFivePointZeroApplication/Startup.cs +++ /dev/null @@ -1,72 +0,0 @@ -using AccessTokenClient; -using AccessTokenClient.Caching; -using AccessTokenClient.Extensions; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace TestingFivePointZeroApplication -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - services.AddMemoryCache(); - - services.AddAccessTokenClient(builder => - { - builder.AddPolicyHandler((provider, _) => - { - var logger = provider.GetService>(); - return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); - }); - }) - .AddAccessTokenClientCache(options => - { - options.ExpirationBuffer = 5; - options.CacheKeyPrefix = "AccessTokenClient"; - }); - - services.AddSingleton(new TestingClientOptions - { - TokenEndpoint = "https://localhost:44342/connect/token", - ClientIdentifier = "client", - ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", - Scopes = new[] { "api1" } - }); - - services - .AddHttpClient() - .AddClientAccessTokenHandler(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) - { - if (environment.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} \ No newline at end of file diff --git a/Projects/TestingFivePointZeroApplication/TestingClient.cs b/Projects/TestingFivePointZeroApplication/TestingClient.cs deleted file mode 100644 index ab67cc1..0000000 --- a/Projects/TestingFivePointZeroApplication/TestingClient.cs +++ /dev/null @@ -1,40 +0,0 @@ -using AccessTokenClient; -using System; -using System.Net.Http; -using System.Threading.Tasks; - -namespace TestingFivePointZeroApplication -{ - public class TestingClient : ITestingClient - { - private readonly HttpClient client; - - public TestingClient(HttpClient client) - { - this.client = client; - } - - public async Task Get() - { - var response = await client.GetAsync(new Uri("https://google.com")); - - return await response.Content.ReadAsStringAsync(); - } - } - - public interface ITestingClient - { - Task Get(); - } - - public class TestingClientOptions : ITokenRequestOptions - { - public string TokenEndpoint { get; set; } - - public string ClientIdentifier { get; set; } - - public string ClientSecret { get; set; } - - public string[] Scopes { get; set; } - } -} \ No newline at end of file diff --git a/Projects/TestingSixPointZeroApplication/Controllers/ValuesController.cs b/Projects/TestingSixPointZeroApplication/Controllers/ValuesController.cs new file mode 100644 index 0000000..1a56c57 --- /dev/null +++ b/Projects/TestingSixPointZeroApplication/Controllers/ValuesController.cs @@ -0,0 +1,55 @@ +using AccessTokenClient; +using Microsoft.AspNetCore.Mvc; + +namespace TestingSixPointZeroApplication.Controllers; + +[ApiController] +[Route("values")] +public class ValuesController : ControllerBase +{ + private readonly ITokenClient client; + + public ValuesController(ITokenClient client) + { + this.client = client; + } + + [HttpGet] + public IActionResult Get() + { + return Ok(); + } +} + +public class TestingClient : ITestingClient +{ + private readonly HttpClient client; + + public TestingClient(HttpClient client) + { + this.client = client; + } + + public async Task Get() + { + var response = await client.GetAsync(new Uri("https://google.com")); + + return await response.Content.ReadAsStringAsync(); + } +} + +public interface ITestingClient +{ + Task Get(); +} + +public class TestingClientOptions : ITokenRequestOptions +{ + public string TokenEndpoint { get; set; } + + public string ClientIdentifier { get; set; } + + public string ClientSecret { get; set; } + + public string[] Scopes { get; set; } +} \ No newline at end of file diff --git a/Projects/TestingSixPointZeroApplication/Program.cs b/Projects/TestingSixPointZeroApplication/Program.cs new file mode 100644 index 0000000..06e44b1 --- /dev/null +++ b/Projects/TestingSixPointZeroApplication/Program.cs @@ -0,0 +1,55 @@ +using AccessTokenClient; +using AccessTokenClient.Caching; +using AccessTokenClient.Extensions; +using TestingSixPointZeroApplication.Controllers; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); + +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "TestingSixPointZeroApplication", Version = "v1" }); +}); + +builder.Services.AddMemoryCache(); + +builder.Services.AddAccessTokenClient(httpClientBuilder => +{ + httpClientBuilder.AddPolicyHandler((provider, _) => + { + var logger = provider.GetService>(); + return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); + }); +}) +.AddAccessTokenClientCache(options => +{ + options.ExpirationBuffer = 5; + options.CacheKeyPrefix = "AccessTokenClient"; +}); + +builder.Services.AddSingleton(new TestingClientOptions +{ + TokenEndpoint = "https://localhost:44303/connect/token", + ClientIdentifier = "testing_client_identifier", + ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", + Scopes = new[] { "employee:read" } +}); + +builder.Services + .AddHttpClient() + .AddClientAccessTokenHandler(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TestingSixPointZeroApplication v1")); +} + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/Projects/TestingFivePointZeroApplication/Properties/launchSettings.json b/Projects/TestingSixPointZeroApplication/Properties/launchSettings.json similarity index 62% rename from Projects/TestingFivePointZeroApplication/Properties/launchSettings.json rename to Projects/TestingSixPointZeroApplication/Properties/launchSettings.json index dfd05f0..9560d1a 100644 --- a/Projects/TestingFivePointZeroApplication/Properties/launchSettings.json +++ b/Projects/TestingSixPointZeroApplication/Properties/launchSettings.json @@ -1,31 +1,31 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", + "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:53711", + "applicationUrl": "http://localhost:25580", "sslPort": 0 } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "TestingSixPointZeroApplication": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "service", + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5114", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TestingFivePointZeroApplication": { - "commandName": "Project", - "dotnetRunMessages": "true", + "IIS Express": { + "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "service", - "applicationUrl": "http://localhost:5000", + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/Projects/TestingFivePointZeroApplication/TestingFivePointZeroApplication.csproj b/Projects/TestingSixPointZeroApplication/TestingSixPointZeroApplication.csproj similarity index 61% rename from Projects/TestingFivePointZeroApplication/TestingFivePointZeroApplication.csproj rename to Projects/TestingSixPointZeroApplication/TestingSixPointZeroApplication.csproj index f151484..6a9c8d1 100644 --- a/Projects/TestingFivePointZeroApplication/TestingFivePointZeroApplication.csproj +++ b/Projects/TestingSixPointZeroApplication/TestingSixPointZeroApplication.csproj @@ -1,12 +1,13 @@ - net5.0 + net6.0 + enable + enable - - + @@ -14,4 +15,4 @@ - +
\ No newline at end of file diff --git a/Projects/TestingSixPointZeroApplication/appsettings.json b/Projects/TestingSixPointZeroApplication/appsettings.json new file mode 100644 index 0000000..ec04bc1 --- /dev/null +++ b/Projects/TestingSixPointZeroApplication/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/Projects/TestingThreePointOneApplication/Controllers/ServiceController.cs b/Projects/TestingThreePointOneApplication/Controllers/ServiceController.cs deleted file mode 100644 index 0601ba6..0000000 --- a/Projects/TestingThreePointOneApplication/Controllers/ServiceController.cs +++ /dev/null @@ -1,35 +0,0 @@ -using AccessTokenClient; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; - -namespace TestingThreePointOneApplication.Controllers -{ - [ApiController] - [Route("service")] - public class ServiceController : ControllerBase - { - private readonly ITokenClient client; - - public ServiceController(ITokenClient client) - { - this.client = client; - } - - [HttpGet] - public async Task Get() - { - var tokenResponse = await client.RequestAccessToken(new TokenRequest - { - TokenEndpoint = "https://localhost:44303/connect/token", - ClientIdentifier = "testing_client_identifier", - ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A", - Scopes = new[] - { - "employee:read", "employee:create", "employee:edit", "employee:delete" - } - }); - - return Ok(tokenResponse); - } - } -} \ No newline at end of file diff --git a/Projects/TestingThreePointOneApplication/Program.cs b/Projects/TestingThreePointOneApplication/Program.cs deleted file mode 100644 index 5cdf76b..0000000 --- a/Projects/TestingThreePointOneApplication/Program.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Serilog; -using Serilog.Events; - -namespace TestingThreePointOneApplication -{ - public class Program - { - public static void Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); - - try - { - Log.Information("Starting the host..."); - CreateHostBuilder(args).Build().Run(); - } - catch (Exception ex) - { - Log.Fatal(ex, "The host terminated unexpectedly"); - } - finally - { - Log.CloseAndFlush(); - } - } - - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args).UseSerilog().ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } - } -} \ No newline at end of file diff --git a/Projects/TestingThreePointOneApplication/Properties/launchSettings.json b/Projects/TestingThreePointOneApplication/Properties/launchSettings.json deleted file mode 100644 index 486575d..0000000 --- a/Projects/TestingThreePointOneApplication/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:61571/", - "sslPort": 44345 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "TestingThreePointOneApplication": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } - } -} \ No newline at end of file diff --git a/Projects/TestingThreePointOneApplication/Startup.cs b/Projects/TestingThreePointOneApplication/Startup.cs deleted file mode 100644 index 8fe5252..0000000 --- a/Projects/TestingThreePointOneApplication/Startup.cs +++ /dev/null @@ -1,56 +0,0 @@ -using AccessTokenClient; -using AccessTokenClient.Caching; -using AccessTokenClient.Extensions; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace TestingThreePointOneApplication -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddMemoryCache(); - - services.AddAccessTokenClient(builderAction: builder => - { - builder.AddPolicyHandler((provider, _) => - { - var logger = provider.GetService>(); - return AccessTokenClientPolicy.GetDefaultRetryPolicy(logger); - }); - }) - .AddAccessTokenClientCache(); - - services.AddControllers(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) - { - if (environment.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} \ No newline at end of file diff --git a/Projects/TestingThreePointOneApplication/TestingThreePointOneApplication.csproj b/Projects/TestingThreePointOneApplication/TestingThreePointOneApplication.csproj deleted file mode 100644 index 6d24f89..0000000 --- a/Projects/TestingThreePointOneApplication/TestingThreePointOneApplication.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - - - - - - - diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 03135f9..6c42ce4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,7 +13,7 @@ steps: displayName: "SDK" inputs: packageType: "sdk" - version: "5.0.100" + version: "6.0.100" - task: DotNetCoreCLI@2 displayName: "Restore" inputs: