Modifying slightly the mocked response, seems to make the output be not parsable. #567

Closed
opened 2025-12-29 08:30:18 +01:00 by adam · 5 comments
Owner

Originally created by @RogerSep on GitHub (Dec 28, 2023).

Originally assigned to: @StefH on GitHub.

I can't understand why tweaking a little bit the json response from my setup, causes tests to fail. Specifically if I add or remove a json property, the json isn't parsable anymore.

There's code at the end of this issue, the test is trying to setup a delegating handler that adds authorization headers to requests made with an http client.

Expected behavior:

Removing the .WithTransformer() or removing some fields from the response shouldn't cause a json to not be parsable.

Test to reproduce

  • I've given a fully working sample below. Can be debugged if needed.
  • In the test snippet, I've added some options that can be commented in/out to see the test fail or work.
  • Comment in and out each option and see the test fail with exception:
    The JSON value could not be converted to TestProject.OAuthHttpHandler+TokenResponse. Path: $ | LineNumber: 0 | BytePositionInLine: 80.

Here's a code snippet of my test. I have a second question, and it's that in the snippet, I don't know how to match form params. One of the forms i've tried is marked with the following comment // How do I make it match form params?, but I've also tried WithParam(), I can't seem to find it.

Using these packages

<PackageReference Include="Correlate" Version="5.2.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/>
<PackageReference Include="WireMock.Net" Version="1.5.45" />
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">

With this test

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Web;
using Correlate;
using FluentAssertions;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;

namespace TestProject;

public class UnitTest1
{
    private readonly WireMockServer _server = WireMockServer.Start();
    
    [Fact]
    public async Task CallingWithOauthClientGeneratesHeaders()
    {
        var accessToken = Guid.NewGuid();
        var correlationContextAccessor = new CorrelationContextAccessor
        {
            CorrelationContext = new CorrelationContext
            {
                CorrelationId = Guid.NewGuid().ToString()
            }
        };
        var oAuthSettings = new OAuthSettings
        {
            ClientId = Guid.NewGuid().ToString(),
            ClientSecret = Guid.NewGuid().ToString(),
            GrantType = Guid.NewGuid().ToString(),
            TokenEndpoint = _server.Url + "/oauth"
        };

        _server
            .Given(
                Request.Create()
                    .WithPath("/oauth")
                    // How do I make it match form params?
                    // .WithParam("grant_type", oAuthSettings.GrantType)
                    // .WithBody(new Dictionary<string, string>
                    // {
                    //     { "grant_type", oAuthSettings.GrantType },
                    //     { "client_id", oAuthSettings.ClientId },
                    //     { "client_secret", oAuthSettings.ClientSecret },
                    //     { "x-correlation-id", correlationContextAccessor.CorrelationContext.CorrelationId }
                    // })
                    .UsingPost()
            )
            .RespondWith(
                Response.Create()
                    // Option 1: This works
                    .WithBodyAsJson(
                        """
                        {
                          "access_token": "my_fake_oauth_token",
                          "jwt": "{{request.headers.Authorization}}",
                          "expires_in": 10
                        }
                        """)
                    .WithTransformer()
                
                    // Option 2: This doesn't work, only difference is I removed the jwt which I copied from another test
                    // .WithBodyAsJson(
                    //     """
                    //     {
                    //       "access_token": "my_fake_oauth_token",
                    //       "expires_in": 10
                    //     }
                    //     """)
                
                    // Option 3: This is what I would like to do, but somehow doesn't work.
                    // .WithBodyAsJson(
                    //     $$$"""
                    //     {
                    //       "access_token": "{{{accessToken}}}",
                    //       "expires_in": 10
                    //     }
                    //     """)
                    
                    // Option 4: This also works. Why can't I remove the transformer and the handlebars?
                    // .WithBodyAsJson(
                    //     $$$"""
                    //        {
                    //          "access_token": "{{{accessToken}}}",
                    //          "jwt": "{{request.headers.Authorization}}",
                    //          "expires_in": 10
                    //        }
                    //        """)
                    //     .WithTransformer()
                
            );

        _server
            .Given(
                Request.Create()
                    .WithPath("/oauth-protected-route")
                    .WithHeader("x-correlation-id", correlationContextAccessor.CorrelationContext.CorrelationId)
                    // .WithHeader("Authentication", $"Bearer {accessToken}")
                    .UsingAnyMethod()
            )
            .RespondWith(
                Response.Create()
                    .WithBody("You've successfully called with authorization Bearer my_fake_oauth_token")
                    .WithTransformer()
            );

        var client = new HttpClient(new OAuthHttpHandler(correlationContextAccessor, oAuthSettings));

        var response = await client.GetStringAsync(_server.Url + "/oauth-protected-route");

        response.Should().Be("You've successfully called with authorization Bearer my_fake_oauth_token");
    }
} 

// A delegating handler that adds and oauth token before making requests...
public class OAuthHttpHandler : DelegatingHandler
{
    private readonly ICorrelationContextAccessor correlationContextAccessor;
    private string accessToken = null!;
    private DateTime tokenExpiryTime;
    private readonly OAuthSettings settings;

    public OAuthHttpHandler(
        ICorrelationContextAccessor correlationContextAccessor,
        OAuthSettings settings,
        HttpMessageHandler? innerHandler = null
    ) : base(innerHandler ?? new HttpClientHandler())
    {
        this.settings = settings;
        this.correlationContextAccessor = correlationContextAccessor;
    }

    private async Task<string> GetAccessTokenAsync()
    {
        if (!string.IsNullOrEmpty(accessToken) && DateTime.UtcNow < tokenExpiryTime)
        {
            return accessToken;
        }

        using var httpClient = new HttpClient();
        var uriBuilder = new UriBuilder(settings.TokenEndpoint);
        var query = HttpUtility.ParseQueryString(uriBuilder.Query);
        query["sig"] = Signature(settings);
        uriBuilder.Query = query.ToString();
        
        var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri)
        {
            Headers =
            {
                { "x-api-key", settings.ClientId }
            },
            Content = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("grant_type", settings.GrantType),
                new KeyValuePair<string, string>("client_id", settings.ClientId),
                new KeyValuePair<string, string>("client_secret", settings.ClientSecret),
                new KeyValuePair<string, string>("x-correlation-id", correlationContextAccessor.CorrelationContext?.CorrelationId ?? "")
            })
        };

        var response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        var responseContent = await response.Content.ReadFromJsonAsync<TokenResponse>();
        
        tokenExpiryTime = DateTime.UtcNow.AddSeconds(responseContent.ExpiresIn);
        accessToken = responseContent.AccessToken;
        return accessToken;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await GetAccessTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        request.Headers.Add("x-correlation-id", correlationContextAccessor.CorrelationContext?.CorrelationId);
        
        return await base.SendAsync(request, cancellationToken);
    }

    private static string Signature(OAuthSettings settings) => Guid.NewGuid().ToString(); // Trimmed to keep this minimal

    private class TokenResponse
    {
        [JsonPropertyName("access_token")]
        public string? AccessToken { get; set; }

        [JsonPropertyName("expires_in")]
        public int ExpiresIn { get; set; }
    }
}

public class OAuthSettings
{
    public string TokenEndpoint { get; set; }
    public string GrantType { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
}
Originally created by @RogerSep on GitHub (Dec 28, 2023). Originally assigned to: @StefH on GitHub. I can't understand why tweaking a little bit the json response from my setup, causes tests to fail. Specifically if I add or remove a json property, the json isn't parsable anymore. There's code at the end of this issue, the test is trying to setup a delegating handler that adds authorization headers to requests made with an http client. ### Expected behavior: Removing the `.WithTransformer()` or removing some fields from the response shouldn't cause a json to not be parsable. ### Test to reproduce - I've given a fully working sample below. Can be debugged if needed. - In the test snippet, I've added some options that can be commented in/out to see the test fail or work. - Comment in and out each option and see the test fail with exception: `The JSON value could not be converted to TestProject.OAuthHttpHandler+TokenResponse. Path: $ | LineNumber: 0 | BytePositionInLine: 80.` ### Other related info Here's a code snippet of my test. I have a second question, and it's that in the snippet, I don't know how to match form params. One of the forms i've tried is marked with the following comment `// How do I make it match form params?`, but I've also tried `WithParam()`, I can't seem to find it. Using these packages ``` c# <PackageReference Include="Correlate" Version="5.2.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/> <PackageReference Include="WireMock.Net" Version="1.5.45" /> <PackageReference Include="xunit" Version="2.4.2"/> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> ``` With this test ``` c# using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Serialization; using System.Web; using Correlate; using FluentAssertions; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; namespace TestProject; public class UnitTest1 { private readonly WireMockServer _server = WireMockServer.Start(); [Fact] public async Task CallingWithOauthClientGeneratesHeaders() { var accessToken = Guid.NewGuid(); var correlationContextAccessor = new CorrelationContextAccessor { CorrelationContext = new CorrelationContext { CorrelationId = Guid.NewGuid().ToString() } }; var oAuthSettings = new OAuthSettings { ClientId = Guid.NewGuid().ToString(), ClientSecret = Guid.NewGuid().ToString(), GrantType = Guid.NewGuid().ToString(), TokenEndpoint = _server.Url + "/oauth" }; _server .Given( Request.Create() .WithPath("/oauth") // How do I make it match form params? // .WithParam("grant_type", oAuthSettings.GrantType) // .WithBody(new Dictionary<string, string> // { // { "grant_type", oAuthSettings.GrantType }, // { "client_id", oAuthSettings.ClientId }, // { "client_secret", oAuthSettings.ClientSecret }, // { "x-correlation-id", correlationContextAccessor.CorrelationContext.CorrelationId } // }) .UsingPost() ) .RespondWith( Response.Create() // Option 1: This works .WithBodyAsJson( """ { "access_token": "my_fake_oauth_token", "jwt": "{{request.headers.Authorization}}", "expires_in": 10 } """) .WithTransformer() // Option 2: This doesn't work, only difference is I removed the jwt which I copied from another test // .WithBodyAsJson( // """ // { // "access_token": "my_fake_oauth_token", // "expires_in": 10 // } // """) // Option 3: This is what I would like to do, but somehow doesn't work. // .WithBodyAsJson( // $$$""" // { // "access_token": "{{{accessToken}}}", // "expires_in": 10 // } // """) // Option 4: This also works. Why can't I remove the transformer and the handlebars? // .WithBodyAsJson( // $$$""" // { // "access_token": "{{{accessToken}}}", // "jwt": "{{request.headers.Authorization}}", // "expires_in": 10 // } // """) // .WithTransformer() ); _server .Given( Request.Create() .WithPath("/oauth-protected-route") .WithHeader("x-correlation-id", correlationContextAccessor.CorrelationContext.CorrelationId) // .WithHeader("Authentication", $"Bearer {accessToken}") .UsingAnyMethod() ) .RespondWith( Response.Create() .WithBody("You've successfully called with authorization Bearer my_fake_oauth_token") .WithTransformer() ); var client = new HttpClient(new OAuthHttpHandler(correlationContextAccessor, oAuthSettings)); var response = await client.GetStringAsync(_server.Url + "/oauth-protected-route"); response.Should().Be("You've successfully called with authorization Bearer my_fake_oauth_token"); } } // A delegating handler that adds and oauth token before making requests... public class OAuthHttpHandler : DelegatingHandler { private readonly ICorrelationContextAccessor correlationContextAccessor; private string accessToken = null!; private DateTime tokenExpiryTime; private readonly OAuthSettings settings; public OAuthHttpHandler( ICorrelationContextAccessor correlationContextAccessor, OAuthSettings settings, HttpMessageHandler? innerHandler = null ) : base(innerHandler ?? new HttpClientHandler()) { this.settings = settings; this.correlationContextAccessor = correlationContextAccessor; } private async Task<string> GetAccessTokenAsync() { if (!string.IsNullOrEmpty(accessToken) && DateTime.UtcNow < tokenExpiryTime) { return accessToken; } using var httpClient = new HttpClient(); var uriBuilder = new UriBuilder(settings.TokenEndpoint); var query = HttpUtility.ParseQueryString(uriBuilder.Query); query["sig"] = Signature(settings); uriBuilder.Query = query.ToString(); var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri) { Headers = { { "x-api-key", settings.ClientId } }, Content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("grant_type", settings.GrantType), new KeyValuePair<string, string>("client_id", settings.ClientId), new KeyValuePair<string, string>("client_secret", settings.ClientSecret), new KeyValuePair<string, string>("x-correlation-id", correlationContextAccessor.CorrelationContext?.CorrelationId ?? "") }) }; var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadFromJsonAsync<TokenResponse>(); tokenExpiryTime = DateTime.UtcNow.AddSeconds(responseContent.ExpiresIn); accessToken = responseContent.AccessToken; return accessToken; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = await GetAccessTokenAsync(); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Headers.Add("x-correlation-id", correlationContextAccessor.CorrelationContext?.CorrelationId); return await base.SendAsync(request, cancellationToken); } private static string Signature(OAuthSettings settings) => Guid.NewGuid().ToString(); // Trimmed to keep this minimal private class TokenResponse { [JsonPropertyName("access_token")] public string? AccessToken { get; set; } [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } } } public class OAuthSettings { public string TokenEndpoint { get; set; } public string GrantType { get; set; } public string ClientId { get; set; } public string ClientSecret { get; set; } } ```
adam added the question label 2025-12-29 08:30:18 +01:00
adam closed this issue 2025-12-29 08:30:18 +01:00
Author
Owner

@StefH commented on GitHub (Dec 28, 2023):

1]
For "Form", you can take a look at this test case WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher

2]
To be able to use "jwt": "{{request.headers.Authorization}}", you need to add .WithTransformer to enable HandleBars text replacement.

@StefH commented on GitHub (Dec 28, 2023): 1] For "Form", you can take a look at this test case `WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher` 2] To be able to use `"jwt": "{{request.headers.Authorization}}"`, you need to add `.WithTransformer` to enable HandleBars text replacement.
Author
Owner

@RogerSep commented on GitHub (Jan 2, 2024):

Thanks for your response.

  1. I made it as suggested and worked. Thanks!
  2. is different, I sent the option that is currently working but it's not what I want, if you inspect the commented code, you can comment out option 1 and un-comment option 3, that option doesn't seem to produce parsable output don't understand why.
@RogerSep commented on GitHub (Jan 2, 2024): Thanks for your response. 1. I made it as suggested and worked. Thanks! 2. is different, I sent the option that is currently working but it's not what I want, if you inspect the commented code, you can comment out option 1 and un-comment option 3, that option doesn't seem to produce parsable output don't understand why.
Author
Owner

@StefH commented on GitHub (Jan 2, 2024):

When using WithBodyAsJson, you do not need to pass a string value, you can just pass in an object.

Like

WithBodyAsJson(new 
{
  access_token = accessToken,
  expires_in = 10
})
@StefH commented on GitHub (Jan 2, 2024): When using `WithBodyAsJson`, you do not need to pass a string value, you can just pass in an object. Like ``` c# WithBodyAsJson(new { access_token = accessToken, expires_in = 10 }) ```
Author
Owner

@StefH commented on GitHub (Jan 8, 2024):

@RogerSep
Did you try my previous comment?

@StefH commented on GitHub (Jan 8, 2024): @RogerSep Did you try my previous comment?
Author
Owner

@RogerSep commented on GitHub (Jan 9, 2024):

Hello, I just got back to this line of work, it did work. Thanks a lot.

Not sure why the string wouldn't work, but your option worked, thanks!

@RogerSep commented on GitHub (Jan 9, 2024): Hello, I just got back to this line of work, it did work. Thanks a lot. Not sure why the string wouldn't work, but your option worked, thanks!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/WireMock.Net#567